More Seamless JS Interop
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.
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
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.
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