dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

More Seamless JS Interop

Open mcmah309 opened this issue 8 months ago • 3 comments

Feature Request

Edit: For anyone finding this issue checkout - dioxus-use-js

We should improve the experience for executing js code. The current way is not very ergonomic, especially with all the escaping. e.g.

pub async fn get_visible_html_content(root_id: Option<&str>) -> Result<Vec<String>> {
    let js = format!( r#"
function getVisibleText(rootElementId = null) {{
	let rootElement = null;
	
	if (rootElementId) {{
		// If specific ID provided, use that element
		rootElement = document.getElementById(rootElementId);
	}} else if (document.body) {{
		// Use body if available
		rootElement = document.body;
	}} else {{
		// Fallback: find first non-head element
		const allRootElements = document.querySelectorAll(':root > *');
		for (const el of allRootElements) {{
			if (el.tagName.toLowerCase() !== 'head') {{
				rootElement = el;
				break;
			}}
		}} 
	}}
	
	if (!rootElement) {{
		throw new Error('No suitable root element found');
	}}
	
	// Query elements within the specified root
	const elements = rootElement.querySelectorAll('*');
	const visibleElements = Array.from(elements).filter(el => {{
		// Skip script and style elements entirely
		if (el.tagName.toLowerCase() === 'script' || 
			el.tagName.toLowerCase() === 'style' ||
			el.tagName.toLowerCase() === 'noscript') {{
			return false;
		}}
		
		// Skip elements with display:none or visibility:hidden
		const style = window.getComputedStyle(el);
		if (style.display === 'none' || style.visibility === 'hidden') {{
			return false;
		}}
		
		// Check if element is in viewport
		const rect = el.getBoundingClientRect();
		return (
			rect.top >= 0 &&
			rect.left >= 0 &&
			rect.bottom <= window.innerHeight &&
			rect.right <= window.innerWidth
		);
	}});
	
	// Extract only text content from visible elements
	const visibleText = visibleElements.map(el => {{
		// Get direct text content of this element (excluding child elements)
		let textContent = '';
		for (let node of el.childNodes) {{
			if (node.nodeType === Node.TEXT_NODE) {{
				const trimmedText = node.textContent.trim();
				if (trimmedText) {{
					textContent += trimmedText + ' ';
				}}
			}}
		}}
		return textContent.trim();
	}}).filter(text => text !== '');
	
	return visibleText;
}}
    
const visibleText = getVisibleText({});

return visibleText;
        "#,
	root_id.map(|e| format!("\"{e}\"")).unwrap_or(String::new())
    );
    let eval = document::eval(js.as_str());

    serde_json::from_value(eval.await?)
}

A better way would be to use macros with the existing manganis system:

pub async fn get_visible_html_content(root_id: Option<&str>) -> Result<Vec<String>> {
    document::module_eval!(
        "path/to/asset.js", // asset that is loaded as a module
        "getVisibleText", // function to invoke inside the module
        root_id // arguments
    ).await?
}

Expands to something like:

pub async fn get_visible_html_content(root_id: Option<&str>) -> Result<Vec<String>> {
    {
	const MODULE: Asset = asset!("path/to/asset.js");
	let js = const_format::formatcp!(r#"
            import getVisibleText from "{MODULE}";
            let arg1 = await dioxus.recv();
	    return getVisibleText(arg1);
	"#);
	let eval = document::eval(js);
	eval.send(root_id)?;

        serde_json::from_value(eval.await?)
    }.await?
}

path/to/asset.js

function getVisibleText(rootElementId = null) {
	let rootElement = null;
	
	if (rootElementId) {
		// If specific ID provided, use that element
		rootElement = document.getElementById(rootElementId);
	} else if (document.body) {
		// Use body if available
		rootElement = document.body;
	} else {
		// Fallback: find first non-head element
		const allRootElements = document.querySelectorAll(':root > *');
		for (const el of allRootElements) {
			if (el.tagName.toLowerCase() !== 'head') {
				rootElement = el;
				break;
			}
		} 
	}
	
	if (!rootElement) {
		throw new Error('No suitable root element found');
	}
	
	// Query elements within the specified root
	const elements = rootElement.querySelectorAll('*');
	const visibleElements = Array.from(elements).filter(el => {
		// Skip script and style elements entirely
		if (el.tagName.toLowerCase() === 'script' || 
			el.tagName.toLowerCase() === 'style' ||
			el.tagName.toLowerCase() === 'noscript') {
			return false;
		}
		
		// Skip elements with display:none or visibility:hidden
		const style = window.getComputedStyle(el);
		if (style.display === 'none' || style.visibility === 'hidden') {
			return false;
		}
		
		// Check if element is in viewport
		const rect = el.getBoundingClientRect();
		return (
			rect.top >= 0 &&
			rect.left >= 0 &&
			rect.bottom <= window.innerHeight &&
			rect.right <= window.innerWidth
		);
	});
	
	// Extract only text content from visible elements
	const visibleText = visibleElements.map(el => {
		// Get direct text content of this element (excluding child elements)
		let textContent = '';
		for (let node of el.childNodes) {
			if (node.nodeType === Node.TEXT_NODE) {
				const trimmedText = node.textContent.trim();
				if (trimmedText) {
					textContent += trimmedText + ' ';
				}
			}
		}
		return textContent.trim();
	}).filter(text => text !== '');
	
	return visibleText;
}

This new proposed alternative allows loading the js code once, no escaping, easy integration of modules in js files, auto generate send/recv, compile time validation (js is valid, function exists, and number of args are correct), and ide/syntax highlighting support since js files are used.

A future version could even support callbacks. e.g.

    document::module_eval!(
        "path/to/asset.js",
        "functionName",
        arg1,
        callback!(arg2),
        callback!(arg3)
    ).await?

For args with callback!, the necessary wrapping code would be generated and use dioxus.send, dioxus.recv, eval.send, eval.recv for communication.

I am interested in implementing this if accepted.

mcmah309 avatar Jun 20 '25 07:06 mcmah309

I agree js interop is currently very messy. One of the potential features we have discussed for 0.8 is implementing a wasm-bindgen compatible macro that works on both desktop and web. Keeping the API compatible with wasm bindgen would let you use it with any library that uses wasm bindgen like plotters, glow and wgpu we would otherwise need to fork to use our own js interop api

ealmloff avatar Jun 20 '25 13:06 ealmloff

Sounds like a good idea too. I added this feature as a PR in the sdk https://github.com/DioxusLabs/sdk/pull/87 . Let me know if you think this would be better in the dioxus itself.

mcmah309 avatar Jun 21 '25 11:06 mcmah309

I ended up creating a PR for the use_js! which I think is better approach than call_js! - https://github.com/DioxusLabs/dioxus/pull/4309

mcmah309 avatar Jun 22 '25 10:06 mcmah309