DioxusLabs / DioxusLabs/dioxus

More Seamless JS Interop

Open
#4,302 3 comments 0 reactions 1 assignee Claimed by @ealmloff View on GitHub
enhancement
Dominant language
Rust
Stars
39.1k
Forks
1.9k
Avg merge
4d 10h
Merged PRs (30d)
4

Description

## Feature Request

**Edit:** For anyone finding this issue checkout - [dioxus-use-js](https://github.com/mcmah309/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.
```rust
pub async fn get_visible_html_content(root_id: Option<&str>) -> Result> {
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:
```rust
pub async fn get_visible_html_content(root_id: Option<&str>) -> Result> {
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:
```rust
pub async fn get_visible_html_content(root_id: Option<&str>) -> Result> {
{
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`
```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.
```rust
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.**

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.