Skip to content

More Seamless JS Interop #4302

Open
Open
@mcmah309

Description

@mcmah309

Feature Request

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions