mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 20:23:09 +00:00
Move the document trait into a separate crate (#3035)
* add a default head method through eval * remove the old document trait * implement document for each platform * pull out document into a dedicated crate to cut down on shared dependencies --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
parent
2b219c826c
commit
519ec9d294
91 changed files with 1113 additions and 1044 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -2380,6 +2380,7 @@ dependencies = [
|
||||||
"dioxus-core-macro",
|
"dioxus-core-macro",
|
||||||
"dioxus-desktop",
|
"dioxus-desktop",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
|
"dioxus-document",
|
||||||
"dioxus-fullstack",
|
"dioxus-fullstack",
|
||||||
"dioxus-hooks",
|
"dioxus-hooks",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
|
@ -2579,6 +2580,7 @@ dependencies = [
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
|
"dioxus-document",
|
||||||
"dioxus-hooks",
|
"dioxus-hooks",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
|
@ -2638,6 +2640,24 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dioxus-document"
|
||||||
|
version = "0.6.0-alpha.2"
|
||||||
|
dependencies = [
|
||||||
|
"dioxus",
|
||||||
|
"dioxus-core",
|
||||||
|
"dioxus-core-macro",
|
||||||
|
"dioxus-core-types",
|
||||||
|
"dioxus-html",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"generational-box",
|
||||||
|
"lazy-js-bundle",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-examples"
|
name = "dioxus-examples"
|
||||||
version = "0.6.0-alpha.2"
|
version = "0.6.0-alpha.2"
|
||||||
|
@ -2815,6 +2835,7 @@ dependencies = [
|
||||||
"dioxus-config-macro",
|
"dioxus-config-macro",
|
||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-core-macro",
|
"dioxus-core-macro",
|
||||||
|
"dioxus-document",
|
||||||
"dioxus-hooks",
|
"dioxus-hooks",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-rsx",
|
"dioxus-rsx",
|
||||||
|
@ -2830,6 +2851,7 @@ dependencies = [
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
|
"dioxus-document",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -3074,6 +3096,7 @@ dependencies = [
|
||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-core-types",
|
"dioxus-core-types",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
|
"dioxus-document",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
"dioxus-signals",
|
"dioxus-signals",
|
||||||
|
|
56
Cargo.toml
56
Cargo.toml
|
@ -1,39 +1,40 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"packages/dioxus",
|
|
||||||
"packages/dioxus-lib",
|
|
||||||
"packages/core",
|
|
||||||
"packages/core-types",
|
|
||||||
"packages/cli",
|
|
||||||
"packages/cli-config",
|
|
||||||
"packages/core-macro",
|
|
||||||
"packages/config-macro",
|
|
||||||
"packages/router-macro",
|
|
||||||
"packages/extension",
|
|
||||||
"packages/router",
|
|
||||||
"packages/html",
|
|
||||||
"packages/html-internal-macro",
|
|
||||||
"packages/hooks",
|
|
||||||
"packages/web",
|
|
||||||
"packages/ssr",
|
|
||||||
"packages/desktop",
|
|
||||||
"packages/mobile",
|
|
||||||
"packages/interpreter",
|
|
||||||
"packages/liveview",
|
|
||||||
"packages/autofmt",
|
"packages/autofmt",
|
||||||
"packages/check",
|
"packages/check",
|
||||||
"packages/rsx",
|
"packages/cli-config",
|
||||||
|
"packages/cli",
|
||||||
|
"packages/config-macro",
|
||||||
|
"packages/core-macro",
|
||||||
|
"packages/core-types",
|
||||||
|
"packages/core",
|
||||||
|
"packages/desktop",
|
||||||
|
"packages/devtools-types",
|
||||||
|
"packages/devtools",
|
||||||
|
"packages/dioxus-lib",
|
||||||
|
"packages/dioxus",
|
||||||
|
"packages/document",
|
||||||
|
"packages/extension",
|
||||||
|
"packages/fullstack",
|
||||||
|
"packages/generational-box",
|
||||||
|
"packages/hooks",
|
||||||
|
"packages/html-internal-macro",
|
||||||
|
"packages/html",
|
||||||
|
"packages/interpreter",
|
||||||
|
"packages/lazy-js-bundle",
|
||||||
|
"packages/liveview",
|
||||||
|
"packages/mobile",
|
||||||
|
"packages/router-macro",
|
||||||
|
"packages/router",
|
||||||
"packages/rsx-hotreload",
|
"packages/rsx-hotreload",
|
||||||
"packages/rsx-rosetta",
|
"packages/rsx-rosetta",
|
||||||
"packages/generational-box",
|
"packages/rsx",
|
||||||
"packages/signals",
|
|
||||||
"packages/devtools",
|
|
||||||
"packages/devtools-types",
|
|
||||||
"packages/fullstack",
|
|
||||||
"packages/server-macro",
|
"packages/server-macro",
|
||||||
|
"packages/signals",
|
||||||
|
"packages/ssr",
|
||||||
"packages/static-generation",
|
"packages/static-generation",
|
||||||
"packages/lazy-js-bundle",
|
"packages/web",
|
||||||
|
|
||||||
# Full project examples
|
# Full project examples
|
||||||
"example-projects/fullstack-hackernews",
|
"example-projects/fullstack-hackernews",
|
||||||
|
@ -76,6 +77,7 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.6.0-alpha.0" }
|
||||||
dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.0" }
|
dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.0" }
|
||||||
dioxus-router = { path = "packages/router", version = "0.6.0-alpha.0" }
|
dioxus-router = { path = "packages/router", version = "0.6.0-alpha.0" }
|
||||||
dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.0" }
|
dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.0" }
|
||||||
|
dioxus-document = { path = "packages/document", version = "0.6.0-alpha.0", default-features = false }
|
||||||
dioxus-html = { path = "packages/html", version = "0.6.0-alpha.0", default-features = false }
|
dioxus-html = { path = "packages/html", version = "0.6.0-alpha.0", default-features = false }
|
||||||
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.0" }
|
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.0" }
|
||||||
dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.0" }
|
dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.0" }
|
||||||
|
|
|
@ -3,7 +3,7 @@ use dioxus::prelude::*;
|
||||||
#[component]
|
#[component]
|
||||||
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
|
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link {
|
document::Link {
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: asset!("./public/loading.css")
|
href: asset!("./public/loading.css")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ mod api;
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(|| {
|
dioxus::launch(|| {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link {
|
document::Link {
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: asset!("./public/tailwind.css")
|
href: asset!("./public/tailwind.css")
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,12 @@ fn app() -> Element {
|
||||||
let mut files = use_signal(Files::new);
|
let mut files = use_signal(Files::new);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link {
|
document::Link {
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: asset!("./assets/fileexplorer.css")
|
href: asset!("./assets/fileexplorer.css")
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
head::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
|
document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
|
||||||
header {
|
header {
|
||||||
i { class: "material-icons icon-menu", "menu" }
|
i { class: "material-icons icon-menu", "menu" }
|
||||||
h1 { "Files: " {files.read().current()} }
|
h1 { "Files: " {files.read().current()} }
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub fn App() -> Element {
|
||||||
#[component]
|
#[component]
|
||||||
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
|
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: asset!("./assets/hackernews.css") }
|
document::Link { rel: "stylesheet", href: asset!("./assets/hackernews.css") }
|
||||||
div { display: "flex", flex_direction: "row", width: "100%",
|
div { display: "flex", flex_direction: "row", width: "100%",
|
||||||
div {
|
div {
|
||||||
width: "50%",
|
width: "50%",
|
||||||
|
|
|
@ -26,7 +26,7 @@ fn app() -> Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
div { id: "container",
|
div { id: "container",
|
||||||
// focusing is necessary to catch keyboard events
|
// focusing is necessary to catch keyboard events
|
||||||
div { id: "receiver", tabindex: 0,
|
div { id: "receiver", tabindex: 0,
|
||||||
|
|
|
@ -54,7 +54,7 @@ fn app() -> Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
div { id: "wrapper",
|
div { id: "wrapper",
|
||||||
div { class: "app",
|
div { class: "app",
|
||||||
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
|
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
|
||||||
|
|
|
@ -29,7 +29,7 @@ fn app() -> Element {
|
||||||
let mut state = use_signal(Calculator::new);
|
let mut state = use_signal(Calculator::new);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: asset!("./examples/assets/calculator.css") }
|
document::Link { rel: "stylesheet", href: asset!("./examples/assets/calculator.css") }
|
||||||
div { id: "wrapper",
|
div { id: "wrapper",
|
||||||
div { class: "app",
|
div { class: "app",
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -36,7 +36,7 @@ fn app() -> Element {
|
||||||
);
|
);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
div { id: "app",
|
div { id: "app",
|
||||||
div { id: "title", "Carpe diem 🎉" }
|
div { id: "title", "Carpe diem 🎉" }
|
||||||
div { id: "clock-display", "{time}" }
|
div { id: "clock-display", "{time}" }
|
||||||
|
|
|
@ -40,7 +40,7 @@ fn app() -> Element {
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
h1 { "Input Roulette" }
|
h1 { "Input Roulette" }
|
||||||
button { onclick: move |_| running.toggle(), "Toggle roulette" }
|
button { onclick: move |_| running.toggle(), "Toggle roulette" }
|
||||||
div { id: "roulette-grid",
|
div { id: "roulette-grid",
|
||||||
|
|
|
@ -16,7 +16,7 @@ fn app() -> Element {
|
||||||
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
|
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
|
|
||||||
div { id: "controls",
|
div { id: "controls",
|
||||||
button { onclick: move |_| counters.write().push(0), "Add counter" }
|
button { onclick: move |_| counters.write().push(0), "Add counter" }
|
||||||
|
|
|
@ -20,13 +20,13 @@ fn main() {
|
||||||
}))
|
}))
|
||||||
.launch(|| {
|
.launch(|| {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link {
|
document::Link {
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: asset!("https://unpkg.com/purecss@2.0.6/build/pure-min.css"),
|
href: asset!("https://unpkg.com/purecss@2.0.6/build/pure-min.css"),
|
||||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
|
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
|
||||||
crossorigin: "anonymous"
|
crossorigin: "anonymous"
|
||||||
}
|
}
|
||||||
head::Link { rel: "stylesheet", href: asset!("./examples/assets/crm.css") }
|
document::Link { rel: "stylesheet", href: asset!("./examples/assets/crm.css") }
|
||||||
h1 { "Dioxus CRM Example" }
|
h1 { "Dioxus CRM Example" }
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ fn app() -> Element {
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
h1 { "Dynamic Assets" }
|
h1 { "Dynamic Assets" }
|
||||||
img { src: "/logos/logo.png" }
|
img { src: "/logos/logo.png" }
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ fn app() -> Element {
|
||||||
// The `eval` is available in the prelude - and simply takes a block of JS.
|
// The `eval` is available in the prelude - and simply takes a block of JS.
|
||||||
// Dioxus' eval is interesting since it allows sending messages to and from the JS code using the `await dioxus.recv()`
|
// Dioxus' eval is interesting since it allows sending messages to and from the JS code using the `await dioxus.recv()`
|
||||||
// builtin function. This allows you to create a two-way communication channel between Rust and JS.
|
// builtin function. This allows you to create a two-way communication channel between Rust and JS.
|
||||||
let mut eval = eval(
|
let mut eval = document::eval(
|
||||||
r#"
|
r#"
|
||||||
dioxus.send("Hi from JS!");
|
dioxus.send("Hi from JS!");
|
||||||
let msg = await dioxus.recv();
|
let msg = await dioxus.recv();
|
||||||
|
@ -29,10 +29,10 @@ fn app() -> Element {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send a message to the JS code.
|
// Send a message to the JS code.
|
||||||
eval.send("Hi from Rust!".into()).unwrap();
|
eval.send("Hi from Rust!").unwrap();
|
||||||
|
|
||||||
// Our line on the JS side will log the message and then return "hello world".
|
// Our line on the JS side will log the message and then return "hello world".
|
||||||
let res = eval.recv().await.unwrap();
|
let res: String = eval.recv().await.unwrap();
|
||||||
|
|
||||||
// This will print "Hi from JS!" and "Hi from Rust!".
|
// This will print "Hi from JS!" and "Hi from Rust!".
|
||||||
println!("{:?}", eval.await);
|
println!("{:?}", eval.await);
|
||||||
|
|
|
@ -43,7 +43,7 @@ fn app() -> Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
|
|
||||||
h1 { "File Upload Example" }
|
h1 { "File Upload Example" }
|
||||||
p { "Drop a .txt, .rs, or .js file here to read it" }
|
p { "Drop a .txt, .rs, or .js file here to read it" }
|
||||||
|
|
|
@ -14,7 +14,7 @@ const STYLE: &str = asset!("./examples/assets/flat_router.css");
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(|| {
|
dioxus::launch(|| {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn main() {
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
Increment {}
|
Increment {}
|
||||||
Decrement {}
|
Decrement {}
|
||||||
Reset {}
|
Reset {}
|
||||||
|
|
|
@ -36,7 +36,7 @@ fn app() -> Element {
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
|
document::Link { rel: "stylesheet", href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
|
||||||
div { class: "container",
|
div { class: "container",
|
||||||
div { class: "columns",
|
div { class: "columns",
|
||||||
div { class: "column",
|
div { class: "column",
|
||||||
|
|
|
@ -16,7 +16,7 @@ fn main() {
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
rsx! (
|
rsx! (
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,23 +12,23 @@ fn app() -> Element {
|
||||||
// You can use the Meta component to render a meta tag into the head of the page
|
// You can use the Meta component to render a meta tag into the head of the page
|
||||||
// Meta tags are useful to provide information about the page to search engines and social media sites
|
// Meta tags are useful to provide information about the page to search engines and social media sites
|
||||||
// This example sets up meta tags for the open graph protocol for social media previews
|
// This example sets up meta tags for the open graph protocol for social media previews
|
||||||
Meta {
|
document::Meta {
|
||||||
property: "og:title",
|
property: "og:title",
|
||||||
content: "My Site",
|
content: "My Site",
|
||||||
}
|
}
|
||||||
Meta {
|
document::Meta {
|
||||||
property: "og:type",
|
property: "og:type",
|
||||||
content: "website",
|
content: "website",
|
||||||
}
|
}
|
||||||
Meta {
|
document::Meta {
|
||||||
property: "og:url",
|
property: "og:url",
|
||||||
content: "https://www.example.com",
|
content: "https://www.example.com",
|
||||||
}
|
}
|
||||||
Meta {
|
document::Meta {
|
||||||
property: "og:image",
|
property: "og:image",
|
||||||
content: "https://example.com/image.jpg",
|
content: "https://example.com/image.jpg",
|
||||||
}
|
}
|
||||||
Meta {
|
document::Meta {
|
||||||
name: "description",
|
name: "description",
|
||||||
content: "My Site is a site",
|
content: "My Site is a site",
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ fn app() -> Element {
|
||||||
_ = use_global_shortcut("cmd+g", move || show_overlay.toggle());
|
_ = use_global_shortcut("cmd+g", move || show_overlay.toggle());
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link {
|
document::Link {
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: asset!("./examples/assets/overlay.css"),
|
href: asset!("./examples/assets/overlay.css"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ fn app() -> Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
head::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
|
document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
|
||||||
div {
|
div {
|
||||||
width: "50%",
|
width: "50%",
|
||||||
height: "50%",
|
height: "50%",
|
||||||
|
|
|
@ -17,7 +17,7 @@ fn app() -> Element {
|
||||||
let mut state = use_signal(|| PlayerState { is_playing: false });
|
let mut state = use_signal(|| PlayerState { is_playing: false });
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
h1 {"Select an option"}
|
h1 {"Select an option"}
|
||||||
|
|
||||||
// Add some cute animations if the radio is playing!
|
// Add some cute animations if the radio is playing!
|
||||||
|
|
|
@ -15,7 +15,7 @@ fn app() -> Element {
|
||||||
let mut dimensions = use_signal(Size2D::zero);
|
let mut dimensions = use_signal(Size2D::zero);
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
head::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
|
document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
|
||||||
div {
|
div {
|
||||||
width: "50%",
|
width: "50%",
|
||||||
height: "50%",
|
height: "50%",
|
||||||
|
|
|
@ -13,7 +13,7 @@ const STYLE: &str = asset!("./examples/assets/router.css");
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(|| {
|
dioxus::launch(|| {
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ fn app() -> Element {
|
||||||
div {
|
div {
|
||||||
// You can set the title of the page with the Title component
|
// You can set the title of the page with the Title component
|
||||||
// In web applications, this sets the title in the head. On desktop, it sets the window title
|
// In web applications, this sets the title in the head. On desktop, it sets the window title
|
||||||
Title { "My Application (Count {count})" }
|
document::Title { "My Application (Count {count})" }
|
||||||
button { onclick: move |_| count += 1, "Up high!" }
|
button { onclick: move |_| count += 1, "Up high!" }
|
||||||
button { onclick: move |_| count -= 1, "Down low!" }
|
button { onclick: move |_| count -= 1, "Down low!" }
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ fn app() -> Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: STYLE }
|
document::Link { rel: "stylesheet", href: STYLE }
|
||||||
section { class: "todoapp",
|
section { class: "todoapp",
|
||||||
TodoHeader { todos }
|
TodoHeader { todos }
|
||||||
section { class: "main",
|
section { class: "main",
|
||||||
|
|
|
@ -19,7 +19,7 @@ fn app() -> Element {
|
||||||
let current_weather = use_resource(move || async move { get_weather(&country()).await });
|
let current_weather = use_resource(move || async move { get_weather(&country()).await });
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
head::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
document::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
||||||
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
|
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
|
||||||
div { class: "flex items-center justify-center flex-row",
|
div { class: "flex items-center justify-center flex-row",
|
||||||
div { class: "flex items-start justify-center flex-row",
|
div { class: "flex items-start justify-center flex-row",
|
||||||
|
|
|
@ -26,7 +26,7 @@ fn main() {
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
rsx!(
|
rsx!(
|
||||||
head::Link { href: "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel: "stylesheet" }
|
document::Link { href: "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel: "stylesheet" }
|
||||||
Header {}
|
Header {}
|
||||||
div { class: "container mx-auto",
|
div { class: "container mx-auto",
|
||||||
div { class: "grid grid-cols-5",
|
div { class: "grid grid-cols-5",
|
||||||
|
|
|
@ -156,7 +156,7 @@ impl BuildRequest {
|
||||||
};
|
};
|
||||||
match variant {
|
match variant {
|
||||||
ResourceType::Style => format!(
|
ResourceType::Style => format!(
|
||||||
" head::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
|
" document::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
|
||||||
path.display()
|
path.display()
|
||||||
),
|
),
|
||||||
ResourceType::Script => {
|
ResourceType::Script => {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
//! use_effect(move || {
|
//! use_effect(move || {
|
||||||
//! let id = id.read();
|
//! let id = id.read();
|
||||||
//! // This will panic if the id is not written to the DOM before the effect is run
|
//! // This will panic if the id is not written to the DOM before the effect is run
|
||||||
//! eval(format!(r#"document.getElementById("{id}").innerHTML = "Hello World";"#));
|
//! document::eval(format!(r#"document.getElementById("{id}").innerHTML = "Hello World";"#));
|
||||||
//! });
|
//! });
|
||||||
//!
|
//!
|
||||||
//! rsx! {
|
//! rsx! {
|
||||||
|
|
|
@ -517,7 +517,7 @@ fn nested_suspense_resolves_client() {
|
||||||
let title = use_resource(move || async_content(0)).suspend()?();
|
let title = use_resource(move || async_content(0)).suspend()?();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
Title { "{title.title}" }
|
document::Title { "{title.title}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ dioxus-html = { workspace = true, features = [
|
||||||
"serialize",
|
"serialize",
|
||||||
"mounted",
|
"mounted",
|
||||||
"file_engine",
|
"file_engine",
|
||||||
"document",
|
|
||||||
] }
|
] }
|
||||||
|
dioxus-document = { workspace = true }
|
||||||
dioxus-signals = { workspace = true, optional = true }
|
dioxus-signals = { workspace = true, optional = true }
|
||||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
|
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
|
||||||
dioxus-cli-config = { workspace = true }
|
dioxus-cli-config = { workspace = true }
|
||||||
|
|
|
@ -16,23 +16,23 @@ static EVALS_RETURNED: GlobalSignal<usize> = Signal::global(|| 0);
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
// Double 100 values in the value
|
// Double 100 values in the value
|
||||||
use_future(|| async {
|
use_future(|| async {
|
||||||
let mut eval = eval(
|
let mut eval = document::eval(
|
||||||
r#"for (let i = 0; i < 100; i++) {
|
r#"for (let i = 0; i < 100; i++) {
|
||||||
let value = await dioxus.recv();
|
let value = await dioxus.recv();
|
||||||
dioxus.send(value*2);
|
dioxus.send(value*2);
|
||||||
}"#,
|
}"#,
|
||||||
);
|
);
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
eval.send(serde_json::Value::from(i)).unwrap();
|
eval.send(i).unwrap();
|
||||||
let value = eval.recv().await.unwrap();
|
let value: i32 = eval.recv().await.unwrap();
|
||||||
assert_eq!(value, serde_json::Value::from(i * 2));
|
assert_eq!(value, i * 2);
|
||||||
EVALS_RECEIVED.with_mut(|x| *x += 1);
|
EVALS_RECEIVED.with_mut(|x| *x += 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure returning no value resolves the future
|
// Make sure returning no value resolves the future
|
||||||
use_future(|| async {
|
use_future(|| async {
|
||||||
let eval = eval(r#"return;"#);
|
let eval = document::eval(r#"return;"#);
|
||||||
|
|
||||||
eval.await.unwrap();
|
eval.await.unwrap();
|
||||||
EVALS_RETURNED.with_mut(|x| *x += 1);
|
EVALS_RETURNED.with_mut(|x| *x += 1);
|
||||||
|
@ -40,7 +40,7 @@ fn app() -> Element {
|
||||||
|
|
||||||
// Return a value from the future
|
// Return a value from the future
|
||||||
use_future(|| async {
|
use_future(|| async {
|
||||||
let eval = eval(
|
let eval = document::eval(
|
||||||
r#"
|
r#"
|
||||||
return [1, 2, 3];
|
return [1, 2, 3];
|
||||||
"#,
|
"#,
|
||||||
|
|
|
@ -16,7 +16,7 @@ fn use_inner_html(id: &'static str) -> Option<String> {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
let res = eval(&format!(
|
let res = document::eval(&format!(
|
||||||
r#"let element = document.getElementById('{}');
|
r#"let element = document.getElementById('{}');
|
||||||
return element.innerHTML"#,
|
return element.innerHTML"#,
|
||||||
id
|
id
|
||||||
|
|
|
@ -50,7 +50,7 @@ pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'sta
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
|
|
||||||
eval(&js).await.unwrap();
|
document::eval(&js).await.unwrap();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use dioxus_html::document::{Document, EvalError, Evaluator};
|
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
||||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
||||||
|
|
||||||
use crate::{query::Query, DesktopContext};
|
use crate::{query::Query, DesktopContext};
|
||||||
|
@ -18,17 +18,13 @@ impl DesktopDocument {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document for DesktopDocument {
|
impl Document for DesktopDocument {
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn eval(&self, js: String) -> Eval {
|
||||||
DesktopEvaluator::create(self.desktop_ctx.clone(), js)
|
Eval::new(DesktopEvaluator::create(self.desktop_ctx.clone(), js))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_title(&self, title: String) {
|
fn set_title(&self, title: String) {
|
||||||
self.desktop_ctx.window.set_title(&title);
|
self.desktop_ctx.window.set_title(&title);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a desktop-target's JavaScript evaluator.
|
/// Represents a desktop-target's JavaScript evaluator.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
[11927251734412729446]
|
[14101548031762241351]
|
|
@ -2,7 +2,7 @@ import {
|
||||||
Channel,
|
Channel,
|
||||||
DioxusChannel,
|
DioxusChannel,
|
||||||
WeakDioxusChannel,
|
WeakDioxusChannel,
|
||||||
} from "../../../html/src/ts/eval";
|
} from "../../../document/src/ts/eval";
|
||||||
|
|
||||||
// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
|
// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -86,5 +86,5 @@ export class NativeDioxusChannel extends DioxusChannel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
|
// Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
|
||||||
async rustRecv(): Promise<any> {}
|
async rustRecv(): Promise<any> { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,9 @@ use crate::{
|
||||||
Config, DesktopContext, DesktopService,
|
Config, DesktopContext, DesktopService,
|
||||||
};
|
};
|
||||||
use dioxus_core::{Runtime, ScopeId, VirtualDom};
|
use dioxus_core::{Runtime, ScopeId, VirtualDom};
|
||||||
|
use dioxus_document::Document;
|
||||||
use dioxus_hooks::to_owned;
|
use dioxus_hooks::to_owned;
|
||||||
use dioxus_html::{prelude::Document, HasFileData, HtmlEvent, PlatformEventData};
|
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
|
||||||
use futures_util::{pin_mut, FutureExt};
|
use futures_util::{pin_mut, FutureExt};
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
@ -13,6 +13,7 @@ rust-version = "1.79.0"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-html = { workspace = true, optional = true }
|
dioxus-html = { workspace = true, optional = true }
|
||||||
|
dioxus-document = { workspace = true, optional = true }
|
||||||
dioxus-core-macro = { workspace = true, optional = true }
|
dioxus-core-macro = { workspace = true, optional = true }
|
||||||
dioxus-config-macro = { workspace = true, optional = true }
|
dioxus-config-macro = { workspace = true, optional = true }
|
||||||
dioxus-hooks = { workspace = true, optional = true }
|
dioxus-hooks = { workspace = true, optional = true }
|
||||||
|
@ -26,7 +27,7 @@ dioxus = { workspace = true }
|
||||||
default = ["macro", "html", "signals", "hooks"]
|
default = ["macro", "html", "signals", "hooks"]
|
||||||
signals = ["dep:dioxus-signals"]
|
signals = ["dep:dioxus-signals"]
|
||||||
macro = ["dep:dioxus-core-macro", "dep:dioxus-rsx", "dep:dioxus-config-macro"]
|
macro = ["dep:dioxus-core-macro", "dep:dioxus-rsx", "dep:dioxus-config-macro"]
|
||||||
html = ["dep:dioxus-html"]
|
html = ["dep:dioxus-html", "dep:dioxus-document"]
|
||||||
hooks = ["dep:dioxus-hooks"]
|
hooks = ["dep:dioxus-hooks"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|
|
@ -16,6 +16,9 @@ pub mod events {
|
||||||
#[cfg(feature = "html")]
|
#[cfg(feature = "html")]
|
||||||
pub use dioxus_html as html;
|
pub use dioxus_html as html;
|
||||||
|
|
||||||
|
#[cfg(feature = "html")]
|
||||||
|
pub use dioxus_document as document;
|
||||||
|
|
||||||
#[cfg(feature = "macro")]
|
#[cfg(feature = "macro")]
|
||||||
pub use dioxus_rsx as rsx;
|
pub use dioxus_rsx as rsx;
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ rust-version = "1.79.0"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-html = { workspace = true, default-features = false, optional = true }
|
dioxus-html = { workspace = true, default-features = false, optional = true }
|
||||||
|
dioxus-document = { workspace = true, optional = true }
|
||||||
dioxus-core-macro = { workspace = true, optional = true }
|
dioxus-core-macro = { workspace = true, optional = true }
|
||||||
dioxus-config-macro = { workspace = true, optional = true }
|
dioxus-config-macro = { workspace = true, optional = true }
|
||||||
dioxus-hooks = { workspace = true, optional = true }
|
dioxus-hooks = { workspace = true, optional = true }
|
||||||
|
@ -44,7 +45,7 @@ devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/de
|
||||||
mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
|
mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
|
||||||
file_engine = ["dioxus-web?/file_engine"]
|
file_engine = ["dioxus-web?/file_engine"]
|
||||||
asset = ["dep:manganis", "dioxus-core/manganis"]
|
asset = ["dep:manganis", "dioxus-core/manganis"]
|
||||||
document = ["dioxus-web?/document", "dioxus-html?/document"]
|
document = ["dioxus-web?/document", "dioxus-document"]
|
||||||
|
|
||||||
launch = ["dep:dioxus-config-macro"]
|
launch = ["dep:dioxus-config-macro"]
|
||||||
router = ["dep:dioxus-router"]
|
router = ["dep:dioxus-router"]
|
||||||
|
|
|
@ -330,7 +330,7 @@ fn web_launch(
|
||||||
#[cfg(all(feature = "static-generation", not(feature = "fullstack")))]
|
#[cfg(all(feature = "static-generation", not(feature = "fullstack")))]
|
||||||
use dioxus_static_site_generation::document;
|
use dioxus_static_site_generation::document;
|
||||||
let document = std::rc::Rc::new(document::web::FullstackWebDocument)
|
let document = std::rc::Rc::new(document::web::FullstackWebDocument)
|
||||||
as std::rc::Rc<dyn crate::prelude::Document>;
|
as std::rc::Rc<dyn crate::prelude::document::Document>;
|
||||||
vdom.provide_root_context(document);
|
vdom.provide_root_context(document);
|
||||||
}
|
}
|
||||||
vdom
|
vdom
|
||||||
|
|
|
@ -50,6 +50,10 @@ pub mod events {
|
||||||
pub use dioxus_html::prelude::*;
|
pub use dioxus_html::prelude::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "document")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
||||||
|
pub use dioxus_document as document;
|
||||||
|
|
||||||
#[cfg(feature = "html")]
|
#[cfg(feature = "html")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||||
pub use dioxus_html as html;
|
pub use dioxus_html as html;
|
||||||
|
@ -59,6 +63,13 @@ pub use dioxus_html as html;
|
||||||
pub use dioxus_core_macro as core_macro;
|
pub use dioxus_core_macro as core_macro;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
#[cfg(feature = "document")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
||||||
|
pub use dioxus_document as document;
|
||||||
|
|
||||||
|
#[cfg(feature = "launch")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||||
|
pub use crate::launch::*;
|
||||||
|
|
||||||
#[cfg(feature = "hooks")]
|
#[cfg(feature = "hooks")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
|
||||||
|
|
22
packages/document/Cargo.toml
Normal file
22
packages/document/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "dioxus-document"
|
||||||
|
edition = "2021"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus-core = { workspace = true }
|
||||||
|
dioxus-core-types = { workspace = true }
|
||||||
|
dioxus-core-macro = { workspace = true }
|
||||||
|
dioxus-html = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
futures-channel = { workspace = true }
|
||||||
|
futures-util.workspace = true
|
||||||
|
generational-box.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
lazy-js-bundle = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dioxus = { workspace = true }
|
1
packages/document/assets/script.js
Normal file
1
packages/document/assets/script.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// this script is included as an asset!() for a test
|
1
packages/document/assets/style.css
Normal file
1
packages/document/assets/style.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/* this stylesheet is included as an asset!() for a test */
|
|
@ -1,6 +1,6 @@
|
||||||
# Communicating with JavaScript
|
# Communicating with JavaScript
|
||||||
|
|
||||||
You can use the `eval` function to execute JavaScript code in your application with the desktop, mobile, web or liveview renderers. Eval takes a block of JavaScript code (that may be asynchronous) and returns a `UseEval` object that you can use to send data to the JavaScript code and receive data from it.
|
You can use the `eval` function to execute JavaScript code in your application with the desktop, mobile, web or liveview renderers. Eval takes a block of JavaScript code (that may be asynchronous) and returns a `Eval` object that you can use to send data to the JavaScript code and receive data from it.
|
||||||
|
|
||||||
<div class="warning">
|
<div class="warning">
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ fn App() -> Element {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| async move {
|
onclick: move |_| async move {
|
||||||
// Eval is a global function you can use anywhere inside Dioxus. It will execute the given JavaScript code.
|
// Eval is a global function you can use anywhere inside Dioxus. It will execute the given JavaScript code.
|
||||||
let result = eval(r#"console.log("Hello World");
|
let result = document::eval(r#"console.log("Hello World");
|
||||||
return "Hello World";"#);
|
return "Hello World";"#);
|
||||||
|
|
||||||
// You can use the `await` keyword to wait for the result of the JavaScript code.
|
// You can use the `await` keyword to wait for the result of the JavaScript code.
|
||||||
|
@ -32,7 +32,7 @@ fn App() -> Element {
|
||||||
|
|
||||||
## Sending data to JavaScript
|
## Sending data to JavaScript
|
||||||
|
|
||||||
When you execute JavaScript code with `eval`, you can pass data to it by formatting the value into the JavaScript code or sending values to the `UseEval` channel.
|
When you execute JavaScript code with `eval`, you can pass data to it by formatting the value into the JavaScript code or sending values to the `Eval` channel.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
@ -43,7 +43,7 @@ fn app() -> Element {
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
// You can pass initial data to the eval function by formatting it into the JavaScript code.
|
// You can pass initial data to the eval function by formatting it into the JavaScript code.
|
||||||
const LOOP_COUNT: usize = 10;
|
const LOOP_COUNT: usize = 10;
|
||||||
let eval = eval(&format!(r#"for(let i = 0; i < {LOOP_COUNT}; i++) {{
|
let eval = document::eval(&format!(r#"for(let i = 0; i < {LOOP_COUNT}; i++) {{
|
||||||
// You can receive values asynchronously with the the `await dioxus.recv()` method.
|
// You can receive values asynchronously with the the `await dioxus.recv()` method.
|
||||||
let value = await dioxus.recv();
|
let value = await dioxus.recv();
|
||||||
console.log("Received", value);
|
console.log("Received", value);
|
||||||
|
@ -51,7 +51,7 @@ fn app() -> Element {
|
||||||
|
|
||||||
// You can send values from rust to the JavaScript code with the `send` method on the object returned by `eval`.
|
// You can send values from rust to the JavaScript code with the `send` method on the object returned by `eval`.
|
||||||
for i in 0..LOOP_COUNT {
|
for i in 0..LOOP_COUNT {
|
||||||
eval.send(i.into()).unwrap();
|
eval.send(i).unwrap();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Log Count"
|
"Log Count"
|
||||||
|
@ -62,7 +62,7 @@ fn app() -> Element {
|
||||||
|
|
||||||
## Sending data from JavaScript
|
## Sending data from JavaScript
|
||||||
|
|
||||||
The `UseEval` struct also contains methods for receiving values you send from JavaScript. You can use the `dioxus.send()` method to send values to the JavaScript code and the `UseEval::recv()` method to receive values from the JavaScript code.
|
The `Eval` struct also contains methods for receiving values you send from JavaScript. You can use the `dioxus.send()` method to send values to the JavaScript code and the `Eval::recv()` method to receive values from the JavaScript code.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
@ -72,14 +72,14 @@ fn app() -> Element {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| async move {
|
onclick: move |_| async move {
|
||||||
// You can send values from rust to the JavaScript code by using the `send` method on the object returned by `eval`.
|
// You can send values from rust to the JavaScript code by using the `send` method on the object returned by `eval`.
|
||||||
let mut eval = eval(r#"for(let i = 0; i < 10; i++) {
|
let mut eval = document::eval(r#"for(let i = 0; i < 10; i++) {
|
||||||
// You can send values asynchronously with the `dioxus.send()` method.
|
// You can send values asynchronously with the `dioxus.send()` method.
|
||||||
dioxus.send(i);
|
dioxus.send(i);
|
||||||
}"#);
|
}"#);
|
||||||
|
|
||||||
// You can receive values from the JavaScript code with the `recv` method on the object returned by `eval`.
|
// You can receive values from the JavaScript code with the `recv` method on the object returned by `eval`.
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let value = eval.recv().await.unwrap();
|
let value: i32 = eval.recv().await.unwrap();
|
||||||
println!("Received {}", value);
|
println!("Received {}", value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -104,12 +104,12 @@ const SCRIPT: &str = r#"
|
||||||
|
|
||||||
fn app() -> Element {
|
fn app() -> Element {
|
||||||
// ❌ You shouldn't run eval in the body of a component. This will run before the component has been mounted
|
// ❌ You shouldn't run eval in the body of a component. This will run before the component has been mounted
|
||||||
// eval(SCRIPT);
|
// document::eval(SCRIPT);
|
||||||
|
|
||||||
// ✅ You should run eval inside an effect or event. This will run after the component has been mounted
|
// ✅ You should run eval inside an effect or event. This will run after the component has been mounted
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
spawn(async {
|
spawn(async {
|
||||||
let count = eval(SCRIPT).await;
|
let count = document::eval(SCRIPT).await;
|
||||||
println!("Count is {:?}", count);
|
println!("Count is {:?}", count);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -4,7 +4,7 @@ Dioxus includes a series of components that render into the head of the page:
|
||||||
|
|
||||||
- [Title](crate::Title)
|
- [Title](crate::Title)
|
||||||
- [Meta](crate::Meta)
|
- [Meta](crate::Meta)
|
||||||
- [head::Link](crate::head::Link)
|
- [document::Link](crate::document::Link)
|
||||||
- [Script](crate::Script)
|
- [Script](crate::Script)
|
||||||
- [Style](crate::Style)
|
- [Style](crate::Style)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ fn RedirectToDioxusHomepageWithoutJS() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
// You can use the meta component to render a meta tag into the head of the page
|
// You can use the meta component to render a meta tag into the head of the page
|
||||||
// This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
// This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
||||||
Meta {
|
document::Meta {
|
||||||
http_equiv: "refresh",
|
http_equiv: "refresh",
|
||||||
content: "10;url=https://dioxuslabs.com",
|
content: "10;url=https://dioxuslabs.com",
|
||||||
}
|
}
|
||||||
|
@ -46,12 +46,12 @@ If you have any important metadata that you want to render into the head, make s
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
// This will render in SSR
|
// This will render in SSR
|
||||||
Title { "My Page" }
|
document::Title { "My Page" }
|
||||||
SuspenseBoundary {
|
SuspenseBoundary {
|
||||||
fallback: |_| rsx! { "Loading..." },
|
fallback: |_| rsx! { "Loading..." },
|
||||||
LoadData {
|
LoadData {
|
||||||
// This will only be rendered on the client after hydration so it may not be visible to search engines
|
// This will only be rendered on the client after hydration so it may not be visible to search engines
|
||||||
Meta { name: "description", content: "My Page" }
|
document::Meta { name: "description", content: "My Page" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
156
packages/document/src/document.rs
Normal file
156
packages/document/src/document.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A context for the document
|
||||||
|
pub type DocumentContext = Arc<dyn Document>;
|
||||||
|
|
||||||
|
fn format_string_for_js(s: &str) -> String {
|
||||||
|
let escaped = s
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\n', "\\n")
|
||||||
|
.replace('\r', "\\r")
|
||||||
|
.replace('"', "\\\"");
|
||||||
|
format!("\"{escaped}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_attributes(attributes: &[(&str, String)]) -> String {
|
||||||
|
let mut formatted = String::from("[");
|
||||||
|
for (key, value) in attributes {
|
||||||
|
formatted.push_str(&format!(
|
||||||
|
"[{}, {}],",
|
||||||
|
format_string_for_js(key),
|
||||||
|
format_string_for_js(value)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if formatted.ends_with(',') {
|
||||||
|
formatted.pop();
|
||||||
|
}
|
||||||
|
formatted.push(']');
|
||||||
|
formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_element_in_head(
|
||||||
|
tag: &str,
|
||||||
|
attributes: &[(&str, String)],
|
||||||
|
children: Option<String>,
|
||||||
|
) -> String {
|
||||||
|
let helpers = include_str!("./js/head.js");
|
||||||
|
let attributes = format_attributes(attributes);
|
||||||
|
let children = children
|
||||||
|
.as_deref()
|
||||||
|
.map(format_string_for_js)
|
||||||
|
.unwrap_or("null".to_string());
|
||||||
|
let tag = format_string_for_js(tag);
|
||||||
|
format!(r#"{helpers};window.createElementInHead({tag}, {attributes}, {children});"#)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A provider for document-related functionality.
|
||||||
|
///
|
||||||
|
/// Provides things like a history API, a title, a way to run JS, and some other basics/essentials used
|
||||||
|
/// by nearly every platform.
|
||||||
|
///
|
||||||
|
/// An integration with some kind of navigation history.
|
||||||
|
///
|
||||||
|
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
||||||
|
/// is fine, as long as both `current_route` and `current_query` match the described format.
|
||||||
|
///
|
||||||
|
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
||||||
|
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||||
|
/// know. Deviations might confuse them.
|
||||||
|
pub trait Document: 'static {
|
||||||
|
/// Run `eval` against this document, returning an [`Eval`] that can be used to await the result.
|
||||||
|
fn eval(&self, js: String) -> Eval;
|
||||||
|
|
||||||
|
/// Set the title of the document
|
||||||
|
fn set_title(&self, title: String) {
|
||||||
|
self.eval(format!("document.title = {title:?};"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new element in the head
|
||||||
|
fn create_head_element(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
attributes: &[(&str, String)],
|
||||||
|
contents: Option<String>,
|
||||||
|
) {
|
||||||
|
self.eval(create_element_in_head(name, attributes, contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new meta tag in the head
|
||||||
|
fn create_meta(&self, props: MetaProps) {
|
||||||
|
let attributes = props.attributes();
|
||||||
|
self.create_head_element("meta", &attributes, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new script tag in the head
|
||||||
|
fn create_script(&self, props: ScriptProps) {
|
||||||
|
let attributes = props.attributes();
|
||||||
|
match (&props.src, props.script_contents()) {
|
||||||
|
// The script has inline contents, render it as a script tag
|
||||||
|
(_, Ok(contents)) => self.create_head_element("script", &attributes, Some(contents)),
|
||||||
|
// The script has a src, render it as a script tag without a body
|
||||||
|
(Some(_), _) => self.create_head_element("script", &attributes, None),
|
||||||
|
// The script has neither contents nor src, log an error
|
||||||
|
(None, Err(err)) => err.log("Script"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new style tag in the head
|
||||||
|
fn create_style(&self, props: StyleProps) {
|
||||||
|
let mut attributes = props.attributes();
|
||||||
|
match (&props.href, props.style_contents()) {
|
||||||
|
// The style has inline contents, render it as a style tag
|
||||||
|
(_, Ok(contents)) => self.create_head_element("style", &attributes, Some(contents)),
|
||||||
|
// The style has a src, render it as a link tag
|
||||||
|
(Some(_), _) => {
|
||||||
|
attributes.push(("type", "text/css".into()));
|
||||||
|
self.create_head_element("link", &attributes, None)
|
||||||
|
}
|
||||||
|
// The style has neither contents nor src, log an error
|
||||||
|
(None, Err(err)) => err.log("Style"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new link tag in the head
|
||||||
|
fn create_link(&self, props: LinkProps) {
|
||||||
|
let attributes = props.attributes();
|
||||||
|
self.create_head_element("link", &attributes, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A document that does nothing
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NoOpDocument;
|
||||||
|
|
||||||
|
impl Document for NoOpDocument {
|
||||||
|
fn eval(&self, _: String) -> Eval {
|
||||||
|
let owner = generational_box::Owner::default();
|
||||||
|
let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
|
||||||
|
Eval::new(boxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An evaluator that does nothing
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NoOpEvaluator;
|
||||||
|
|
||||||
|
impl Evaluator for NoOpEvaluator {
|
||||||
|
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
|
||||||
|
Err(EvalError::Unsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_recv(
|
||||||
|
&mut self,
|
||||||
|
_context: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||||
|
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_join(
|
||||||
|
&mut self,
|
||||||
|
_context: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||||
|
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||||
|
}
|
||||||
|
}
|
126
packages/document/src/elements/link.rs
Normal file
126
packages/document/src/elements/link.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::document;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Props, PartialEq)]
|
||||||
|
pub struct LinkProps {
|
||||||
|
pub rel: Option<String>,
|
||||||
|
pub media: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub disabled: Option<bool>,
|
||||||
|
pub r#as: Option<String>,
|
||||||
|
pub sizes: Option<String>,
|
||||||
|
/// Links are deduplicated by their href attribute
|
||||||
|
pub href: Option<String>,
|
||||||
|
pub crossorigin: Option<String>,
|
||||||
|
pub referrerpolicy: Option<String>,
|
||||||
|
pub fetchpriority: Option<String>,
|
||||||
|
pub hreflang: Option<String>,
|
||||||
|
pub integrity: Option<String>,
|
||||||
|
pub r#type: Option<String>,
|
||||||
|
pub blocking: Option<String>,
|
||||||
|
#[props(extends = link, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinkProps {
|
||||||
|
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||||
|
let mut attributes = Vec::new();
|
||||||
|
if let Some(rel) = &self.rel {
|
||||||
|
attributes.push(("rel", rel.clone()));
|
||||||
|
}
|
||||||
|
if let Some(media) = &self.media {
|
||||||
|
attributes.push(("media", media.clone()));
|
||||||
|
}
|
||||||
|
if let Some(title) = &self.title {
|
||||||
|
attributes.push(("title", title.clone()));
|
||||||
|
}
|
||||||
|
if let Some(disabled) = &self.disabled {
|
||||||
|
attributes.push(("disabled", disabled.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(r#as) = &self.r#as {
|
||||||
|
attributes.push(("as", r#as.clone()));
|
||||||
|
}
|
||||||
|
if let Some(sizes) = &self.sizes {
|
||||||
|
attributes.push(("sizes", sizes.clone()));
|
||||||
|
}
|
||||||
|
if let Some(href) = &self.href {
|
||||||
|
attributes.push(("href", href.clone()));
|
||||||
|
}
|
||||||
|
if let Some(crossorigin) = &self.crossorigin {
|
||||||
|
attributes.push(("crossOrigin", crossorigin.clone()));
|
||||||
|
}
|
||||||
|
if let Some(referrerpolicy) = &self.referrerpolicy {
|
||||||
|
attributes.push(("referrerPolicy", referrerpolicy.clone()));
|
||||||
|
}
|
||||||
|
if let Some(fetchpriority) = &self.fetchpriority {
|
||||||
|
attributes.push(("fetchPriority", fetchpriority.clone()));
|
||||||
|
}
|
||||||
|
if let Some(hreflang) = &self.hreflang {
|
||||||
|
attributes.push(("hrefLang", hreflang.clone()));
|
||||||
|
}
|
||||||
|
if let Some(integrity) = &self.integrity {
|
||||||
|
attributes.push(("integrity", integrity.clone()));
|
||||||
|
}
|
||||||
|
if let Some(r#type) = &self.r#type {
|
||||||
|
attributes.push(("type", r#type.clone()));
|
||||||
|
}
|
||||||
|
if let Some(blocking) = &self.blocking {
|
||||||
|
attributes.push(("blocking", blocking.clone()));
|
||||||
|
}
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a [`link`](crate::elements::link) tag into the head of the page.
|
||||||
|
///
|
||||||
|
/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
|
||||||
|
/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn RedBackground() -> Element {
|
||||||
|
/// rsx! {
|
||||||
|
/// // You can use the meta component to render a meta tag into the head of the page
|
||||||
|
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
||||||
|
/// document::Link {
|
||||||
|
/// href: asset!("./assets/style.css"),
|
||||||
|
/// rel: "stylesheet",
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// <div class="warning">
|
||||||
|
///
|
||||||
|
/// Any updates to the props after the first render will not be reflected in the head.
|
||||||
|
///
|
||||||
|
/// </div>
|
||||||
|
#[doc(alias = "<link>")]
|
||||||
|
#[component]
|
||||||
|
pub fn Link(props: LinkProps) -> Element {
|
||||||
|
use_update_warning(&props, "Link {}");
|
||||||
|
|
||||||
|
use_hook(|| {
|
||||||
|
if let Some(href) = &props.href {
|
||||||
|
if !should_insert_link(href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let document = document();
|
||||||
|
document.create_link(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
VNode::empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct LinkContext(DeduplicationContext);
|
||||||
|
|
||||||
|
fn should_insert_link(href: &str) -> bool {
|
||||||
|
get_or_insert_root_context::<LinkContext>()
|
||||||
|
.0
|
||||||
|
.should_insert(href)
|
||||||
|
}
|
74
packages/document/src/elements/meta.rs
Normal file
74
packages/document/src/elements/meta.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::document;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
/// Props for the [`Meta`] component
|
||||||
|
#[derive(Clone, Props, PartialEq)]
|
||||||
|
pub struct MetaProps {
|
||||||
|
pub property: Option<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub charset: Option<String>,
|
||||||
|
pub http_equiv: Option<String>,
|
||||||
|
pub content: Option<String>,
|
||||||
|
#[props(extends = meta, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetaProps {
|
||||||
|
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||||
|
let mut attributes = Vec::new();
|
||||||
|
if let Some(property) = &self.property {
|
||||||
|
attributes.push(("property", property.clone()));
|
||||||
|
}
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
attributes.push(("name", name.clone()));
|
||||||
|
}
|
||||||
|
if let Some(charset) = &self.charset {
|
||||||
|
attributes.push(("charset", charset.clone()));
|
||||||
|
}
|
||||||
|
if let Some(http_equiv) = &self.http_equiv {
|
||||||
|
attributes.push(("http-equiv", http_equiv.clone()));
|
||||||
|
}
|
||||||
|
if let Some(content) = &self.content {
|
||||||
|
attributes.push(("content", content.clone()));
|
||||||
|
}
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a [`meta`](crate::elements::meta) tag into the head of the page.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn RedirectToDioxusHomepageWithoutJS() -> Element {
|
||||||
|
/// rsx! {
|
||||||
|
/// // You can use the meta component to render a meta tag into the head of the page
|
||||||
|
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
||||||
|
/// document::Meta {
|
||||||
|
/// http_equiv: "refresh",
|
||||||
|
/// content: "10;url=https://dioxuslabs.com",
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// <div class="warning">
|
||||||
|
///
|
||||||
|
/// Any updates to the props after the first render will not be reflected in the head.
|
||||||
|
///
|
||||||
|
/// </div>
|
||||||
|
#[component]
|
||||||
|
#[doc(alias = "<meta>")]
|
||||||
|
pub fn Meta(props: MetaProps) -> Element {
|
||||||
|
use_update_warning(&props, "Meta {}");
|
||||||
|
|
||||||
|
use_hook(|| {
|
||||||
|
let document = document();
|
||||||
|
document.create_meta(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
VNode::empty()
|
||||||
|
}
|
124
packages/document/src/elements/mod.rs
Normal file
124
packages/document/src/elements/mod.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
#![doc = include_str!("../../docs/head.md")]
|
||||||
|
|
||||||
|
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
|
use dioxus_core::{prelude::*, DynamicNode};
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
|
||||||
|
mod link;
|
||||||
|
pub use link::*;
|
||||||
|
mod meta;
|
||||||
|
pub use meta::*;
|
||||||
|
mod script;
|
||||||
|
pub use script::*;
|
||||||
|
mod style;
|
||||||
|
pub use style::*;
|
||||||
|
mod title;
|
||||||
|
pub use title::*;
|
||||||
|
|
||||||
|
/// Warn the user if they try to change props on a element that is injected into the head
|
||||||
|
#[allow(unused)]
|
||||||
|
fn use_update_warning<T: PartialEq + Clone + 'static>(value: &T, name: &'static str) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let cloned_value = value.clone();
|
||||||
|
let initial = use_hook(move || value.clone());
|
||||||
|
|
||||||
|
if initial != cloned_value {
|
||||||
|
tracing::warn!("Changing the props of `{name}` is not supported ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that can occur when extracting a single text node from a component
|
||||||
|
pub enum ExtractSingleTextNodeError<'a> {
|
||||||
|
/// The node contained an render error, so we can't extract the text node
|
||||||
|
RenderError(&'a RenderError),
|
||||||
|
/// There was only one child, but it wasn't a text node
|
||||||
|
NonTextNode,
|
||||||
|
/// There is multiple child nodes
|
||||||
|
NonTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractSingleTextNodeError<'_> {
|
||||||
|
/// Log a warning depending on the error
|
||||||
|
pub fn log(&self, component: &str) {
|
||||||
|
match self {
|
||||||
|
ExtractSingleTextNodeError::RenderError(err) => {
|
||||||
|
tracing::error!("Error while rendering {component}: {err}");
|
||||||
|
}
|
||||||
|
ExtractSingleTextNodeError::NonTextNode => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error while rendering {component}: The children of {component} must be a single text node"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ExtractSingleTextNodeError::NonTemplate => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error while rendering {component}: The children of {component} must be a single text node"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_single_text_node(children: &Element) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||||
|
let vnode = match children {
|
||||||
|
Element::Ok(vnode) => vnode,
|
||||||
|
Element::Err(err) => {
|
||||||
|
return Err(ExtractSingleTextNodeError::RenderError(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// The title's children must be in one of two forms:
|
||||||
|
// 1. rsx! { "static text" }
|
||||||
|
// 2. rsx! { "title: {dynamic_text}" }
|
||||||
|
match vnode.template {
|
||||||
|
// rsx! { "static text" }
|
||||||
|
Template {
|
||||||
|
roots: &[TemplateNode::Text { text }],
|
||||||
|
node_paths: &[],
|
||||||
|
attr_paths: &[],
|
||||||
|
..
|
||||||
|
} => Ok(text.to_string()),
|
||||||
|
// rsx! { "title: {dynamic_text}" }
|
||||||
|
Template {
|
||||||
|
roots: &[TemplateNode::Dynamic { id }],
|
||||||
|
node_paths: &[&[0]],
|
||||||
|
attr_paths: &[],
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let node = &vnode.dynamic_nodes[id];
|
||||||
|
match node {
|
||||||
|
DynamicNode::Text(text) => Ok(text.value.clone()),
|
||||||
|
_ => Err(ExtractSingleTextNodeError::NonTextNode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(ExtractSingleTextNodeError::NonTemplate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
|
||||||
|
match ScopeId::ROOT.has_context::<T>() {
|
||||||
|
Some(context) => context,
|
||||||
|
None => {
|
||||||
|
let context = T::default();
|
||||||
|
ScopeId::ROOT.provide_context(context.clone());
|
||||||
|
context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
|
||||||
|
|
||||||
|
impl DeduplicationContext {
|
||||||
|
fn should_insert(&self, href: &str) -> bool {
|
||||||
|
let mut set = self.0.borrow_mut();
|
||||||
|
let present = set.contains(href);
|
||||||
|
if !present {
|
||||||
|
set.insert(href.to_string());
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
packages/document/src/elements/script.rs
Normal file
113
packages/document/src/elements/script.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::document;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Props, PartialEq)]
|
||||||
|
pub struct ScriptProps {
|
||||||
|
/// The contents of the script tag. If present, the children must be a single text node.
|
||||||
|
pub children: Element,
|
||||||
|
/// Scripts are deduplicated by their src attribute
|
||||||
|
pub src: Option<String>,
|
||||||
|
pub defer: Option<bool>,
|
||||||
|
pub crossorigin: Option<String>,
|
||||||
|
pub fetchpriority: Option<String>,
|
||||||
|
pub integrity: Option<String>,
|
||||||
|
pub nomodule: Option<bool>,
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
pub referrerpolicy: Option<String>,
|
||||||
|
pub r#type: Option<String>,
|
||||||
|
#[props(extends = script, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptProps {
|
||||||
|
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||||
|
let mut attributes = Vec::new();
|
||||||
|
if let Some(defer) = &self.defer {
|
||||||
|
attributes.push(("defer", defer.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(crossorigin) = &self.crossorigin {
|
||||||
|
attributes.push(("crossorigin", crossorigin.clone()));
|
||||||
|
}
|
||||||
|
if let Some(fetchpriority) = &self.fetchpriority {
|
||||||
|
attributes.push(("fetchpriority", fetchpriority.clone()));
|
||||||
|
}
|
||||||
|
if let Some(integrity) = &self.integrity {
|
||||||
|
attributes.push(("integrity", integrity.clone()));
|
||||||
|
}
|
||||||
|
if let Some(nomodule) = &self.nomodule {
|
||||||
|
attributes.push(("nomodule", nomodule.to_string()));
|
||||||
|
}
|
||||||
|
if let Some(nonce) = &self.nonce {
|
||||||
|
attributes.push(("nonce", nonce.clone()));
|
||||||
|
}
|
||||||
|
if let Some(referrerpolicy) = &self.referrerpolicy {
|
||||||
|
attributes.push(("referrerpolicy", referrerpolicy.clone()));
|
||||||
|
}
|
||||||
|
if let Some(r#type) = &self.r#type {
|
||||||
|
attributes.push(("type", r#type.clone()));
|
||||||
|
}
|
||||||
|
if let Some(src) = &self.src {
|
||||||
|
attributes.push(("src", src.clone()));
|
||||||
|
}
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||||
|
extract_single_text_node(&self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a [`script`](crate::elements::script) tag into the head of the page.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// If present, the children of the script component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the script will not be added.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Any scripts you add will be deduplicated by their `src` attribute (if present).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn LoadScript() -> Element {
|
||||||
|
/// rsx! {
|
||||||
|
/// // You can use the Script component to render a script tag into the head of the page
|
||||||
|
/// document::Script {
|
||||||
|
/// src: asset!("./assets/script.js"),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// <div class="warning">
|
||||||
|
///
|
||||||
|
/// Any updates to the props after the first render will not be reflected in the head.
|
||||||
|
///
|
||||||
|
/// </div>
|
||||||
|
#[component]
|
||||||
|
pub fn Script(props: ScriptProps) -> Element {
|
||||||
|
use_update_warning(&props, "Script {}");
|
||||||
|
|
||||||
|
use_hook(|| {
|
||||||
|
if let Some(src) = &props.src {
|
||||||
|
if !should_insert_script(src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let document = document();
|
||||||
|
document.create_script(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
VNode::empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct ScriptContext(DeduplicationContext);
|
||||||
|
|
||||||
|
fn should_insert_script(src: &str) -> bool {
|
||||||
|
get_or_insert_root_context::<ScriptContext>()
|
||||||
|
.0
|
||||||
|
.should_insert(src)
|
||||||
|
}
|
93
packages/document/src/elements/style.rs
Normal file
93
packages/document/src/elements/style.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::document;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Props, PartialEq)]
|
||||||
|
pub struct StyleProps {
|
||||||
|
/// Styles are deduplicated by their href attribute
|
||||||
|
pub href: Option<String>,
|
||||||
|
pub media: Option<String>,
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// The contents of the style tag. If present, the children must be a single text node.
|
||||||
|
pub children: Element,
|
||||||
|
#[props(extends = style, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleProps {
|
||||||
|
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||||
|
let mut attributes = Vec::new();
|
||||||
|
if let Some(href) = &self.href {
|
||||||
|
attributes.push(("href", href.clone()));
|
||||||
|
}
|
||||||
|
if let Some(media) = &self.media {
|
||||||
|
attributes.push(("media", media.clone()));
|
||||||
|
}
|
||||||
|
if let Some(nonce) = &self.nonce {
|
||||||
|
attributes.push(("nonce", nonce.clone()));
|
||||||
|
}
|
||||||
|
if let Some(title) = &self.title {
|
||||||
|
attributes.push(("title", title.clone()));
|
||||||
|
}
|
||||||
|
attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||||
|
extract_single_text_node(&self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a [`style`](crate::elements::style) tag into the head of the page.
|
||||||
|
///
|
||||||
|
/// If present, the children of the style component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the style will not be added.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn RedBackground() -> Element {
|
||||||
|
/// rsx! {
|
||||||
|
/// // You can use the style component to render a style tag into the head of the page
|
||||||
|
/// // This style tag will set the background color of the page to red
|
||||||
|
/// document::Style {
|
||||||
|
/// r#"
|
||||||
|
/// body {{
|
||||||
|
/// background-color: red;
|
||||||
|
/// }}
|
||||||
|
/// "#
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// <div class="warning">
|
||||||
|
///
|
||||||
|
/// Any updates to the props after the first render will not be reflected in the head.
|
||||||
|
///
|
||||||
|
/// </div>
|
||||||
|
#[component]
|
||||||
|
pub fn Style(props: StyleProps) -> Element {
|
||||||
|
use_update_warning(&props, "Style {}");
|
||||||
|
|
||||||
|
use_hook(|| {
|
||||||
|
if let Some(href) = &props.href {
|
||||||
|
if !should_insert_style(href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let document = document();
|
||||||
|
document.create_style(props);
|
||||||
|
});
|
||||||
|
|
||||||
|
VNode::empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct StyleContext(DeduplicationContext);
|
||||||
|
|
||||||
|
fn should_insert_style(href: &str) -> bool {
|
||||||
|
get_or_insert_root_context::<StyleContext>()
|
||||||
|
.0
|
||||||
|
.should_insert(href)
|
||||||
|
}
|
57
packages/document/src/elements/title.rs
Normal file
57
packages/document/src/elements/title.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use crate::document;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Props, PartialEq)]
|
||||||
|
pub struct TitleProps {
|
||||||
|
/// The contents of the title tag. The children must be a single text node.
|
||||||
|
children: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the title of the page. On web renderers, this will set the [title](crate::elements::title) in the head. On desktop, it will set the window title.
|
||||||
|
///
|
||||||
|
/// Unlike most head components, the Title can be modified after the first render. Only the latest update to the title will be reflected if multiple title components are rendered.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// The children of the title component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the title will not be updated.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn App() -> Element {
|
||||||
|
/// rsx! {
|
||||||
|
/// // You can use the Title component to render a title tag into the head of the page or window
|
||||||
|
/// document::Title { "My Page" }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
#[doc(alias = "<title>")]
|
||||||
|
pub fn Title(props: TitleProps) -> Element {
|
||||||
|
let children = props.children;
|
||||||
|
let text = match extract_single_text_node(&children) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(err) => {
|
||||||
|
err.log("Title");
|
||||||
|
return VNode::empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
|
||||||
|
let document = use_hook(document);
|
||||||
|
let last_text = use_hook(|| {
|
||||||
|
// Set the title initially
|
||||||
|
document.set_title(text.clone());
|
||||||
|
Rc::new(RefCell::new(text.clone()))
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the text changes, update the title
|
||||||
|
let mut last_text = last_text.borrow_mut();
|
||||||
|
if text != *last_text {
|
||||||
|
document.set_title(text.clone());
|
||||||
|
*last_text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
VNode::empty()
|
||||||
|
}
|
36
packages/document/src/error.rs
Normal file
36
packages/document/src/error.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
/// Represents an error when evaluating JavaScript
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum EvalError {
|
||||||
|
/// The platform does not support evaluating JavaScript.
|
||||||
|
Unsupported,
|
||||||
|
|
||||||
|
/// The provided JavaScript has already been ran.
|
||||||
|
Finished,
|
||||||
|
|
||||||
|
/// The provided JavaScript is not valid and can't be ran.
|
||||||
|
InvalidJs(String),
|
||||||
|
|
||||||
|
/// Represents an error communicating between JavaScript and Rust.
|
||||||
|
Communication(String),
|
||||||
|
|
||||||
|
/// Represents an error serializing or deserializing the result of an eval
|
||||||
|
Serialization(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for EvalError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
EvalError::Unsupported => write!(f, "EvalError::Unsupported - eval is not supported on the current platform"),
|
||||||
|
EvalError::Finished => write!(f, "EvalError::Finished - eval has already ran"),
|
||||||
|
EvalError::InvalidJs(_) => write!(f, "EvalError::InvalidJs - the provided javascript is invalid"),
|
||||||
|
EvalError::Communication(_) => write!(f, "EvalError::Communication - there was an error trying to communicate with between javascript and rust"),
|
||||||
|
EvalError::Serialization(_) => write!(f, "EvalError::Serialization - there was an error trying to serialize or deserialize the result of an eval"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for EvalError {}
|
74
packages/document/src/eval.rs
Normal file
74
packages/document/src/eval.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
#![doc = include_str!("../docs/eval.md")]
|
||||||
|
|
||||||
|
use crate::error::EvalError;
|
||||||
|
use generational_box::GenerationalBox;
|
||||||
|
use std::future::{poll_fn, Future, IntoFuture};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
#[doc = include_str!("../docs/eval.md")]
|
||||||
|
pub struct Eval {
|
||||||
|
evaluator: GenerationalBox<Box<dyn Evaluator>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eval {
|
||||||
|
/// Create this eval from a dynamic evaluator
|
||||||
|
pub fn new(evaluator: GenerationalBox<Box<dyn Evaluator + 'static>>) -> Self {
|
||||||
|
Self { evaluator }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait until the javascript task is finished and return the result
|
||||||
|
pub async fn join<T: serde::de::DeserializeOwned>(self) -> Result<T, EvalError> {
|
||||||
|
let json_value = poll_fn(|cx| match self.evaluator.try_write() {
|
||||||
|
Ok(mut evaluator) => evaluator.poll_join(cx),
|
||||||
|
Err(_) => Poll::Ready(Err(EvalError::Finished)),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
serde_json::from_value(json_value).map_err(EvalError::Serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message to the javascript task
|
||||||
|
pub fn send(&self, data: impl serde::Serialize) -> Result<(), EvalError> {
|
||||||
|
match self.evaluator.try_read() {
|
||||||
|
Ok(evaluator) => {
|
||||||
|
evaluator.send(serde_json::to_value(data).map_err(EvalError::Serialization)?)
|
||||||
|
}
|
||||||
|
Err(_) => Err(EvalError::Finished),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive a message from the javascript task
|
||||||
|
pub async fn recv<T: serde::de::DeserializeOwned>(&mut self) -> Result<T, EvalError> {
|
||||||
|
let json_value = poll_fn(|cx| match self.evaluator.try_write() {
|
||||||
|
Ok(mut evaluator) => evaluator.poll_recv(cx),
|
||||||
|
Err(_) => Poll::Ready(Err(EvalError::Finished)),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
serde_json::from_value(json_value).map_err(EvalError::Serialization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoFuture for Eval {
|
||||||
|
type Output = Result<serde_json::Value, EvalError>;
|
||||||
|
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
Box::pin(self.join().into_future())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The platform's evaluator.
|
||||||
|
pub trait Evaluator {
|
||||||
|
/// Sends a message to the evaluated JavaScript.
|
||||||
|
fn send(&self, data: serde_json::Value) -> Result<(), EvalError>;
|
||||||
|
/// Receive any queued messages from the evaluated JavaScript.
|
||||||
|
fn poll_recv(
|
||||||
|
&mut self,
|
||||||
|
context: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<serde_json::Value, EvalError>>;
|
||||||
|
/// Gets the return value of the JavaScript
|
||||||
|
fn poll_join(
|
||||||
|
&mut self,
|
||||||
|
context: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<serde_json::Value, EvalError>>;
|
||||||
|
}
|
31
packages/document/src/lib.rs
Normal file
31
packages/document/src/lib.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
mod document;
|
||||||
|
mod elements;
|
||||||
|
mod error;
|
||||||
|
mod eval;
|
||||||
|
|
||||||
|
pub use document::*;
|
||||||
|
pub use elements::*;
|
||||||
|
pub use error::*;
|
||||||
|
pub use eval::*;
|
||||||
|
|
||||||
|
/// Get the document provider for the current platform or a no-op provider if the platform doesn't document functionality.
|
||||||
|
pub fn document() -> Rc<dyn Document> {
|
||||||
|
match dioxus_core::prelude::try_consume_context::<Rc<dyn Document>>() {
|
||||||
|
Some(document) => document,
|
||||||
|
None => {
|
||||||
|
tracing::error!(
|
||||||
|
"Unable to find a document in the renderer. Using the default no-op document."
|
||||||
|
);
|
||||||
|
Rc::new(NoOpDocument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate some javascript in the current document
|
||||||
|
#[doc = include_str!("../docs/eval.md")]
|
||||||
|
#[doc(alias = "javascript")]
|
||||||
|
pub fn eval(script: &str) -> Eval {
|
||||||
|
document().eval(script.to_string())
|
||||||
|
}
|
18
packages/document/tsconfig.json
Normal file
18
packages/document/tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"lib": [
|
||||||
|
"ES2015",
|
||||||
|
"DOM",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -4,9 +4,8 @@
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use dioxus_lib::{html::document::*, prelude::*};
|
use dioxus_lib::{document::*, prelude::*};
|
||||||
use dioxus_ssr::Renderer;
|
use dioxus_ssr::Renderer;
|
||||||
use generational_box::GenerationalBox;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
@ -71,8 +70,8 @@ impl ServerDocument {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document for ServerDocument {
|
impl Document for ServerDocument {
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn eval(&self, js: String) -> Eval {
|
||||||
NoOpDocument.new_evaluator(js)
|
NoOpDocument.eval(js)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_title(&self, title: String) {
|
fn set_title(&self, title: String) {
|
||||||
|
@ -151,7 +150,7 @@ impl Document for ServerDocument {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_link(&self, props: head::LinkProps) {
|
fn create_link(&self, props: LinkProps) {
|
||||||
self.warn_if_streaming();
|
self.warn_if_streaming();
|
||||||
self.serialize_for_hydration();
|
self.serialize_for_hydration();
|
||||||
self.0.borrow_mut().link.push(rsx! {
|
self.0.borrow_mut().link.push(rsx! {
|
||||||
|
@ -173,8 +172,4 @@ impl Document for ServerDocument {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
//! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
|
//! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
|
||||||
|
|
||||||
use dioxus_lib::events::Document;
|
use dioxus_lib::document::*;
|
||||||
use dioxus_web::WebDocument;
|
use dioxus_web::WebDocument;
|
||||||
|
|
||||||
fn head_element_written_on_server() -> bool {
|
fn head_element_written_on_server() -> bool {
|
||||||
|
@ -15,11 +15,8 @@ fn head_element_written_on_server() -> bool {
|
||||||
pub struct FullstackWebDocument;
|
pub struct FullstackWebDocument;
|
||||||
|
|
||||||
impl Document for FullstackWebDocument {
|
impl Document for FullstackWebDocument {
|
||||||
fn new_evaluator(
|
fn eval(&self, js: String) -> Eval {
|
||||||
&self,
|
WebDocument.eval(js)
|
||||||
js: String,
|
|
||||||
) -> generational_box::GenerationalBox<Box<dyn dioxus_lib::prelude::document::Evaluator>> {
|
|
||||||
WebDocument.new_evaluator(js)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_title(&self, title: String) {
|
fn set_title(&self, title: String) {
|
||||||
|
@ -29,35 +26,31 @@ impl Document for FullstackWebDocument {
|
||||||
WebDocument.set_title(title);
|
WebDocument.set_title(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_meta(&self, props: dioxus_lib::prelude::MetaProps) {
|
fn create_meta(&self, props: MetaProps) {
|
||||||
if head_element_written_on_server() {
|
if head_element_written_on_server() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WebDocument.create_meta(props);
|
WebDocument.create_meta(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_script(&self, props: dioxus_lib::prelude::ScriptProps) {
|
fn create_script(&self, props: ScriptProps) {
|
||||||
if head_element_written_on_server() {
|
if head_element_written_on_server() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WebDocument.create_script(props);
|
WebDocument.create_script(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_style(&self, props: dioxus_lib::prelude::StyleProps) {
|
fn create_style(&self, props: StyleProps) {
|
||||||
if head_element_written_on_server() {
|
if head_element_written_on_server() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WebDocument.create_style(props);
|
WebDocument.create_style(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_link(&self, props: dioxus_lib::prelude::head::LinkProps) {
|
fn create_link(&self, props: LinkProps) {
|
||||||
if head_element_written_on_server() {
|
if head_element_written_on_server() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WebDocument.create_link(props);
|
WebDocument.create_link(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
//! A shared pool of renderers for efficient server side rendering.
|
//! A shared pool of renderers for efficient server side rendering.
|
||||||
|
use crate::document::ServerDocument;
|
||||||
use crate::streaming::{Mount, StreamingRenderer};
|
use crate::streaming::{Mount, StreamingRenderer};
|
||||||
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
||||||
use dioxus_isrg::{CachedRender, RenderFreshness};
|
use dioxus_isrg::{CachedRender, RenderFreshness};
|
||||||
|
use dioxus_lib::document::Document;
|
||||||
use dioxus_ssr::Renderer;
|
use dioxus_ssr::Renderer;
|
||||||
use futures_channel::mpsc::Sender;
|
use futures_channel::mpsc::Sender;
|
||||||
use futures_util::{Stream, StreamExt};
|
use futures_util::{Stream, StreamExt};
|
||||||
|
@ -165,6 +167,7 @@ impl SsrRendererPool {
|
||||||
let join_handle = spawn_platform(move || async move {
|
let join_handle = spawn_platform(move || async move {
|
||||||
let mut virtual_dom = virtual_dom_factory();
|
let mut virtual_dom = virtual_dom_factory();
|
||||||
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
|
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
|
||||||
|
virtual_dom.provide_root_context(document.clone());
|
||||||
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
|
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
|
||||||
|
|
||||||
// poll the future, which may call server_context()
|
// poll the future, which may call server_context()
|
||||||
|
@ -431,11 +434,8 @@ impl FullstackHTMLTemplate {
|
||||||
let ServeConfig { index, .. } = &self.cfg;
|
let ServeConfig { index, .. } = &self.cfg;
|
||||||
|
|
||||||
let title = {
|
let title = {
|
||||||
let document: Option<std::rc::Rc<dyn Document>> =
|
let document: Option<std::rc::Rc<ServerDocument>> =
|
||||||
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
|
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
|
||||||
let document: Option<&crate::document::server::ServerDocument> = document
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|document| document.as_any().downcast_ref());
|
|
||||||
// Collect any head content from the document provider and inject that into the head
|
// Collect any head content from the document provider and inject that into the head
|
||||||
document.and_then(|document| document.title())
|
document.and_then(|document| document.title())
|
||||||
};
|
};
|
||||||
|
@ -448,11 +448,8 @@ impl FullstackHTMLTemplate {
|
||||||
}
|
}
|
||||||
to.write_str(&index.head_after_title)?;
|
to.write_str(&index.head_after_title)?;
|
||||||
|
|
||||||
let document: Option<std::rc::Rc<dyn dioxus_lib::prelude::document::Document>> =
|
let document: Option<std::rc::Rc<ServerDocument>> =
|
||||||
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
|
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
|
||||||
let document: Option<&crate::document::server::ServerDocument> = document
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|document| document.as_any().downcast_ref());
|
|
||||||
if let Some(document) = document {
|
if let Some(document) = document {
|
||||||
// Collect any head content from the document provider and inject that into the head
|
// Collect any head content from the document provider and inject that into the head
|
||||||
document.render(to)?;
|
document.render(to)?;
|
||||||
|
|
|
@ -16,7 +16,7 @@ fn MyComponent() -> Element {
|
||||||
let count = count.read();
|
let count = count.read();
|
||||||
|
|
||||||
// You can use the count value to update the DOM manually
|
// You can use the count value to update the DOM manually
|
||||||
eval(&format!(
|
document::eval(&format!(
|
||||||
r#"var c = document.getElementById("dioxus-canvas");
|
r#"var c = document.getElementById("dioxus-canvas");
|
||||||
var ctx = c.getContext("2d");
|
var ctx = c.getContext("2d");
|
||||||
ctx.font = "30px Arial";
|
ctx.font = "30px Arial";
|
||||||
|
|
|
@ -41,7 +41,7 @@ tokio = { workspace = true, features = ["time"] }
|
||||||
manganis = { workspace = true }
|
manganis = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serialize", "mounted", "document", "file_engine"]
|
default = ["serialize", "mounted", "file_engine"]
|
||||||
serialize = [
|
serialize = [
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
"dep:serde_json",
|
"dep:serde_json",
|
||||||
|
@ -51,10 +51,6 @@ serialize = [
|
||||||
"dioxus-core/serialize"
|
"dioxus-core/serialize"
|
||||||
]
|
]
|
||||||
mounted = []
|
mounted = []
|
||||||
document = [
|
|
||||||
"dep:serde",
|
|
||||||
"dep:serde_json"
|
|
||||||
]
|
|
||||||
file_engine = [
|
file_engine = [
|
||||||
"dep:async-trait",
|
"dep:async-trait",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,134 +0,0 @@
|
||||||
#![allow(clippy::await_holding_refcell_ref)]
|
|
||||||
#![doc = include_str!("../../docs/eval.md")]
|
|
||||||
|
|
||||||
use dioxus_core::prelude::*;
|
|
||||||
use generational_box::GenerationalBox;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::future::{poll_fn, Future, IntoFuture};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use super::document;
|
|
||||||
|
|
||||||
/// The platform's evaluator.
|
|
||||||
pub trait Evaluator {
|
|
||||||
/// Sends a message to the evaluated JavaScript.
|
|
||||||
fn send(&self, data: serde_json::Value) -> Result<(), EvalError>;
|
|
||||||
/// Receive any queued messages from the evaluated JavaScript.
|
|
||||||
fn poll_recv(
|
|
||||||
&mut self,
|
|
||||||
context: &mut Context<'_>,
|
|
||||||
) -> Poll<Result<serde_json::Value, EvalError>>;
|
|
||||||
/// Gets the return value of the JavaScript
|
|
||||||
fn poll_join(
|
|
||||||
&mut self,
|
|
||||||
context: &mut Context<'_>,
|
|
||||||
) -> Poll<Result<serde_json::Value, EvalError>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EvalCreator = Rc<dyn Fn(&str) -> UseEval>;
|
|
||||||
|
|
||||||
/// Get a struct that can execute any JavaScript.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// Please be very careful with this function. A script with too many dynamic
|
|
||||||
/// parts is practically asking for a hacker to find an XSS vulnerability in
|
|
||||||
/// it. **This applies especially to web targets, where the JavaScript context
|
|
||||||
/// has access to most, if not all of your application data.**
|
|
||||||
#[must_use]
|
|
||||||
pub fn eval_provider() -> EvalCreator {
|
|
||||||
let eval_provider = document();
|
|
||||||
|
|
||||||
Rc::new(move |script: &str| UseEval::new(eval_provider.new_evaluator(script.to_string())))
|
|
||||||
as Rc<dyn Fn(&str) -> UseEval>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc = include_str!("../../docs/eval.md")]
|
|
||||||
#[doc(alias = "javascript")]
|
|
||||||
pub fn eval(script: &str) -> UseEval {
|
|
||||||
let document = use_hook(document);
|
|
||||||
UseEval::new(document.new_evaluator(script.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper around the target platform's evaluator that lets you send and receive data from JavaScript spawned by [`eval`].
|
|
||||||
///
|
|
||||||
#[doc = include_str!("../../docs/eval.md")]
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct UseEval {
|
|
||||||
evaluator: GenerationalBox<Box<dyn Evaluator>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UseEval {
|
|
||||||
/// Creates a new UseEval
|
|
||||||
pub fn new(evaluator: GenerationalBox<Box<dyn Evaluator + 'static>>) -> Self {
|
|
||||||
Self { evaluator }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a [`serde_json::Value`] to the evaluated JavaScript.
|
|
||||||
pub fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
|
|
||||||
match self.evaluator.try_read() {
|
|
||||||
Ok(evaluator) => evaluator.send(data),
|
|
||||||
Err(_) => Err(EvalError::Finished),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
|
|
||||||
pub async fn recv(&mut self) -> Result<serde_json::Value, EvalError> {
|
|
||||||
poll_fn(|cx| match self.evaluator.try_write() {
|
|
||||||
Ok(mut evaluator) => evaluator.poll_recv(cx),
|
|
||||||
Err(_) => Poll::Ready(Err(EvalError::Finished)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the return value of the evaluated JavaScript.
|
|
||||||
pub async fn join(self) -> Result<serde_json::Value, EvalError> {
|
|
||||||
poll_fn(|cx| match self.evaluator.try_write() {
|
|
||||||
Ok(mut evaluator) => evaluator.poll_join(cx),
|
|
||||||
Err(_) => Poll::Ready(Err(EvalError::Finished)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoFuture for UseEval {
|
|
||||||
type Output = Result<serde_json::Value, EvalError>;
|
|
||||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
|
|
||||||
|
|
||||||
fn into_future(self) -> Self::IntoFuture {
|
|
||||||
Box::pin(self.join())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents an error when evaluating JavaScript
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum EvalError {
|
|
||||||
/// The platform does not support evaluating JavaScript.
|
|
||||||
Unsupported,
|
|
||||||
|
|
||||||
/// The provided JavaScript has already been ran.
|
|
||||||
Finished,
|
|
||||||
|
|
||||||
/// The provided JavaScript is not valid and can't be ran.
|
|
||||||
InvalidJs(String),
|
|
||||||
|
|
||||||
/// Represents an error communicating between JavaScript and Rust.
|
|
||||||
Communication(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for EvalError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
EvalError::Unsupported => write!(f, "EvalError::Unsupported - eval is not supported on the current platform"),
|
|
||||||
EvalError::Finished => write!(f, "EvalError::Finished - eval has already ran"),
|
|
||||||
EvalError::InvalidJs(_) => write!(f, "EvalError::InvalidJs - the provided javascript is invalid"),
|
|
||||||
EvalError::Communication(_) => write!(f, "EvalError::Communication - there was an error trying to communicate with between javascript and rust"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for EvalError {}
|
|
|
@ -1,562 +0,0 @@
|
||||||
#![doc = include_str!("../../docs/head.md")]
|
|
||||||
|
|
||||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
|
||||||
|
|
||||||
use crate as dioxus_elements;
|
|
||||||
use dioxus_core::{prelude::*, DynamicNode};
|
|
||||||
use dioxus_core_macro::*;
|
|
||||||
|
|
||||||
/// Warn the user if they try to change props on a element that is injected into the head
|
|
||||||
#[allow(unused)]
|
|
||||||
fn use_update_warning<T: PartialEq + Clone + 'static>(value: &T, name: &'static str) {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
let cloned_value = value.clone();
|
|
||||||
let initial = use_hook(move || value.clone());
|
|
||||||
|
|
||||||
if initial != cloned_value {
|
|
||||||
tracing::warn!("Changing the props of `{name}` is not supported ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An error that can occur when extracting a single text node from a component
|
|
||||||
pub enum ExtractSingleTextNodeError<'a> {
|
|
||||||
/// The node contained an render error, so we can't extract the text node
|
|
||||||
RenderError(&'a RenderError),
|
|
||||||
/// There was only one child, but it wasn't a text node
|
|
||||||
NonTextNode,
|
|
||||||
/// There is multiple child nodes
|
|
||||||
NonTemplate,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtractSingleTextNodeError<'_> {
|
|
||||||
/// Log a warning depending on the error
|
|
||||||
pub fn log(&self, component: &str) {
|
|
||||||
match self {
|
|
||||||
ExtractSingleTextNodeError::RenderError(err) => {
|
|
||||||
tracing::error!("Error while rendering {component}: {err}");
|
|
||||||
}
|
|
||||||
ExtractSingleTextNodeError::NonTextNode => {
|
|
||||||
tracing::error!(
|
|
||||||
"Error while rendering {component}: The children of {component} must be a single text node"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ExtractSingleTextNodeError::NonTemplate => {
|
|
||||||
tracing::error!(
|
|
||||||
"Error while rendering {component}: The children of {component} must be a single text node"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_single_text_node(children: &Element) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
|
||||||
let vnode = match children {
|
|
||||||
Element::Ok(vnode) => vnode,
|
|
||||||
Element::Err(err) => {
|
|
||||||
return Err(ExtractSingleTextNodeError::RenderError(err));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// The title's children must be in one of two forms:
|
|
||||||
// 1. rsx! { "static text" }
|
|
||||||
// 2. rsx! { "title: {dynamic_text}" }
|
|
||||||
match vnode.template {
|
|
||||||
// rsx! { "static text" }
|
|
||||||
Template {
|
|
||||||
roots: &[TemplateNode::Text { text }],
|
|
||||||
node_paths: &[],
|
|
||||||
attr_paths: &[],
|
|
||||||
..
|
|
||||||
} => Ok(text.to_string()),
|
|
||||||
// rsx! { "title: {dynamic_text}" }
|
|
||||||
Template {
|
|
||||||
roots: &[TemplateNode::Dynamic { id }],
|
|
||||||
node_paths: &[&[0]],
|
|
||||||
attr_paths: &[],
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let node = &vnode.dynamic_nodes[id];
|
|
||||||
match node {
|
|
||||||
DynamicNode::Text(text) => Ok(text.value.clone()),
|
|
||||||
_ => Err(ExtractSingleTextNodeError::NonTextNode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(ExtractSingleTextNodeError::NonTemplate),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Props, PartialEq)]
|
|
||||||
pub struct TitleProps {
|
|
||||||
/// The contents of the title tag. The children must be a single text node.
|
|
||||||
children: Element,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the title of the page. On web renderers, this will set the [title](crate::elements::title) in the head. On desktop, it will set the window title.
|
|
||||||
///
|
|
||||||
/// Unlike most head components, the Title can be modified after the first render. Only the latest update to the title will be reflected if multiple title components are rendered.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// The children of the title component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the title will not be updated.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// fn App() -> Element {
|
|
||||||
/// rsx! {
|
|
||||||
/// // You can use the Title component to render a title tag into the head of the page or window
|
|
||||||
/// Title { "My Page" }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[component]
|
|
||||||
pub fn Title(props: TitleProps) -> Element {
|
|
||||||
let children = props.children;
|
|
||||||
let text = match extract_single_text_node(&children) {
|
|
||||||
Ok(text) => text,
|
|
||||||
Err(err) => {
|
|
||||||
err.log("Title");
|
|
||||||
return VNode::empty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
|
|
||||||
let document = use_hook(document);
|
|
||||||
let last_text = use_hook(|| {
|
|
||||||
// Set the title initially
|
|
||||||
document.set_title(text.clone());
|
|
||||||
Rc::new(RefCell::new(text.clone()))
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the text changes, update the title
|
|
||||||
let mut last_text = last_text.borrow_mut();
|
|
||||||
if text != *last_text {
|
|
||||||
document.set_title(text.clone());
|
|
||||||
*last_text = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
VNode::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
/// Props for the [`Meta`] component
|
|
||||||
#[derive(Clone, Props, PartialEq)]
|
|
||||||
pub struct MetaProps {
|
|
||||||
pub property: Option<String>,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub charset: Option<String>,
|
|
||||||
pub http_equiv: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
#[props(extends = meta, extends = GlobalAttributes)]
|
|
||||||
pub additional_attributes: Vec<Attribute>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MetaProps {
|
|
||||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
|
||||||
let mut attributes = Vec::new();
|
|
||||||
if let Some(property) = &self.property {
|
|
||||||
attributes.push(("property", property.clone()));
|
|
||||||
}
|
|
||||||
if let Some(name) = &self.name {
|
|
||||||
attributes.push(("name", name.clone()));
|
|
||||||
}
|
|
||||||
if let Some(charset) = &self.charset {
|
|
||||||
attributes.push(("charset", charset.clone()));
|
|
||||||
}
|
|
||||||
if let Some(http_equiv) = &self.http_equiv {
|
|
||||||
attributes.push(("http-equiv", http_equiv.clone()));
|
|
||||||
}
|
|
||||||
if let Some(content) = &self.content {
|
|
||||||
attributes.push(("content", content.clone()));
|
|
||||||
}
|
|
||||||
attributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a [`meta`](crate::elements::meta) tag into the head of the page.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// fn RedirectToDioxusHomepageWithoutJS() -> Element {
|
|
||||||
/// rsx! {
|
|
||||||
/// // You can use the meta component to render a meta tag into the head of the page
|
|
||||||
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
|
||||||
/// Meta {
|
|
||||||
/// http_equiv: "refresh",
|
|
||||||
/// content: "10;url=https://dioxuslabs.com",
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// <div class="warning">
|
|
||||||
///
|
|
||||||
/// Any updates to the props after the first render will not be reflected in the head.
|
|
||||||
///
|
|
||||||
/// </div>
|
|
||||||
#[component]
|
|
||||||
pub fn Meta(props: MetaProps) -> Element {
|
|
||||||
use_update_warning(&props, "Meta {}");
|
|
||||||
|
|
||||||
use_hook(|| {
|
|
||||||
let document = document();
|
|
||||||
document.create_meta(props);
|
|
||||||
});
|
|
||||||
|
|
||||||
VNode::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Clone, Props, PartialEq)]
|
|
||||||
pub struct ScriptProps {
|
|
||||||
/// The contents of the script tag. If present, the children must be a single text node.
|
|
||||||
pub children: Element,
|
|
||||||
/// Scripts are deduplicated by their src attribute
|
|
||||||
pub src: Option<String>,
|
|
||||||
pub defer: Option<bool>,
|
|
||||||
pub crossorigin: Option<String>,
|
|
||||||
pub fetchpriority: Option<String>,
|
|
||||||
pub integrity: Option<String>,
|
|
||||||
pub nomodule: Option<bool>,
|
|
||||||
pub nonce: Option<String>,
|
|
||||||
pub referrerpolicy: Option<String>,
|
|
||||||
pub r#type: Option<String>,
|
|
||||||
#[props(extends = script, extends = GlobalAttributes)]
|
|
||||||
pub additional_attributes: Vec<Attribute>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScriptProps {
|
|
||||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
|
||||||
let mut attributes = Vec::new();
|
|
||||||
if let Some(defer) = &self.defer {
|
|
||||||
attributes.push(("defer", defer.to_string()));
|
|
||||||
}
|
|
||||||
if let Some(crossorigin) = &self.crossorigin {
|
|
||||||
attributes.push(("crossorigin", crossorigin.clone()));
|
|
||||||
}
|
|
||||||
if let Some(fetchpriority) = &self.fetchpriority {
|
|
||||||
attributes.push(("fetchpriority", fetchpriority.clone()));
|
|
||||||
}
|
|
||||||
if let Some(integrity) = &self.integrity {
|
|
||||||
attributes.push(("integrity", integrity.clone()));
|
|
||||||
}
|
|
||||||
if let Some(nomodule) = &self.nomodule {
|
|
||||||
attributes.push(("nomodule", nomodule.to_string()));
|
|
||||||
}
|
|
||||||
if let Some(nonce) = &self.nonce {
|
|
||||||
attributes.push(("nonce", nonce.clone()));
|
|
||||||
}
|
|
||||||
if let Some(referrerpolicy) = &self.referrerpolicy {
|
|
||||||
attributes.push(("referrerpolicy", referrerpolicy.clone()));
|
|
||||||
}
|
|
||||||
if let Some(r#type) = &self.r#type {
|
|
||||||
attributes.push(("type", r#type.clone()));
|
|
||||||
}
|
|
||||||
if let Some(src) = &self.src {
|
|
||||||
attributes.push(("src", src.clone()));
|
|
||||||
}
|
|
||||||
attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
|
||||||
extract_single_text_node(&self.children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a [`script`](crate::elements::script) tag into the head of the page.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// If present, the children of the script component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the script will not be added.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// Any scripts you add will be deduplicated by their `src` attribute (if present).
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// fn LoadScript() -> Element {
|
|
||||||
/// rsx! {
|
|
||||||
/// // You can use the Script component to render a script tag into the head of the page
|
|
||||||
/// Script {
|
|
||||||
/// src: asset!("./assets/script.js"),
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// <div class="warning">
|
|
||||||
///
|
|
||||||
/// Any updates to the props after the first render will not be reflected in the head.
|
|
||||||
///
|
|
||||||
/// </div>
|
|
||||||
#[component]
|
|
||||||
pub fn Script(props: ScriptProps) -> Element {
|
|
||||||
use_update_warning(&props, "Script {}");
|
|
||||||
|
|
||||||
use_hook(|| {
|
|
||||||
if let Some(src) = &props.src {
|
|
||||||
if !should_insert_script(src) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let document = document();
|
|
||||||
document.create_script(props);
|
|
||||||
});
|
|
||||||
|
|
||||||
VNode::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Clone, Props, PartialEq)]
|
|
||||||
pub struct StyleProps {
|
|
||||||
/// Styles are deduplicated by their href attribute
|
|
||||||
pub href: Option<String>,
|
|
||||||
pub media: Option<String>,
|
|
||||||
pub nonce: Option<String>,
|
|
||||||
pub title: Option<String>,
|
|
||||||
/// The contents of the style tag. If present, the children must be a single text node.
|
|
||||||
pub children: Element,
|
|
||||||
#[props(extends = style, extends = GlobalAttributes)]
|
|
||||||
pub additional_attributes: Vec<Attribute>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StyleProps {
|
|
||||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
|
||||||
let mut attributes = Vec::new();
|
|
||||||
if let Some(href) = &self.href {
|
|
||||||
attributes.push(("href", href.clone()));
|
|
||||||
}
|
|
||||||
if let Some(media) = &self.media {
|
|
||||||
attributes.push(("media", media.clone()));
|
|
||||||
}
|
|
||||||
if let Some(nonce) = &self.nonce {
|
|
||||||
attributes.push(("nonce", nonce.clone()));
|
|
||||||
}
|
|
||||||
if let Some(title) = &self.title {
|
|
||||||
attributes.push(("title", title.clone()));
|
|
||||||
}
|
|
||||||
attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
|
||||||
extract_single_text_node(&self.children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a [`style`](crate::elements::style) tag into the head of the page.
|
|
||||||
///
|
|
||||||
/// If present, the children of the style component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the style will not be added.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// fn RedBackground() -> Element {
|
|
||||||
/// rsx! {
|
|
||||||
/// // You can use the style component to render a style tag into the head of the page
|
|
||||||
/// // This style tag will set the background color of the page to red
|
|
||||||
/// Style {
|
|
||||||
/// r#"
|
|
||||||
/// body {{
|
|
||||||
/// background-color: red;
|
|
||||||
/// }}
|
|
||||||
/// "#
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// <div class="warning">
|
|
||||||
///
|
|
||||||
/// Any updates to the props after the first render will not be reflected in the head.
|
|
||||||
///
|
|
||||||
/// </div>
|
|
||||||
#[component]
|
|
||||||
pub fn Style(props: StyleProps) -> Element {
|
|
||||||
use_update_warning(&props, "Style {}");
|
|
||||||
|
|
||||||
use_hook(|| {
|
|
||||||
if let Some(href) = &props.href {
|
|
||||||
if !should_insert_style(href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let document = document();
|
|
||||||
document.create_style(props);
|
|
||||||
});
|
|
||||||
|
|
||||||
VNode::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Clone, Props, PartialEq)]
|
|
||||||
pub struct LinkProps {
|
|
||||||
pub rel: Option<String>,
|
|
||||||
pub media: Option<String>,
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub disabled: Option<bool>,
|
|
||||||
pub r#as: Option<String>,
|
|
||||||
pub sizes: Option<String>,
|
|
||||||
/// Links are deduplicated by their href attribute
|
|
||||||
pub href: Option<String>,
|
|
||||||
pub crossorigin: Option<String>,
|
|
||||||
pub referrerpolicy: Option<String>,
|
|
||||||
pub fetchpriority: Option<String>,
|
|
||||||
pub hreflang: Option<String>,
|
|
||||||
pub integrity: Option<String>,
|
|
||||||
pub r#type: Option<String>,
|
|
||||||
pub blocking: Option<String>,
|
|
||||||
#[props(extends = link, extends = GlobalAttributes)]
|
|
||||||
pub additional_attributes: Vec<Attribute>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinkProps {
|
|
||||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
|
||||||
let mut attributes = Vec::new();
|
|
||||||
if let Some(rel) = &self.rel {
|
|
||||||
attributes.push(("rel", rel.clone()));
|
|
||||||
}
|
|
||||||
if let Some(media) = &self.media {
|
|
||||||
attributes.push(("media", media.clone()));
|
|
||||||
}
|
|
||||||
if let Some(title) = &self.title {
|
|
||||||
attributes.push(("title", title.clone()));
|
|
||||||
}
|
|
||||||
if let Some(disabled) = &self.disabled {
|
|
||||||
attributes.push(("disabled", disabled.to_string()));
|
|
||||||
}
|
|
||||||
if let Some(r#as) = &self.r#as {
|
|
||||||
attributes.push(("as", r#as.clone()));
|
|
||||||
}
|
|
||||||
if let Some(sizes) = &self.sizes {
|
|
||||||
attributes.push(("sizes", sizes.clone()));
|
|
||||||
}
|
|
||||||
if let Some(href) = &self.href {
|
|
||||||
attributes.push(("href", href.clone()));
|
|
||||||
}
|
|
||||||
if let Some(crossorigin) = &self.crossorigin {
|
|
||||||
attributes.push(("crossOrigin", crossorigin.clone()));
|
|
||||||
}
|
|
||||||
if let Some(referrerpolicy) = &self.referrerpolicy {
|
|
||||||
attributes.push(("referrerPolicy", referrerpolicy.clone()));
|
|
||||||
}
|
|
||||||
if let Some(fetchpriority) = &self.fetchpriority {
|
|
||||||
attributes.push(("fetchPriority", fetchpriority.clone()));
|
|
||||||
}
|
|
||||||
if let Some(hreflang) = &self.hreflang {
|
|
||||||
attributes.push(("hrefLang", hreflang.clone()));
|
|
||||||
}
|
|
||||||
if let Some(integrity) = &self.integrity {
|
|
||||||
attributes.push(("integrity", integrity.clone()));
|
|
||||||
}
|
|
||||||
if let Some(r#type) = &self.r#type {
|
|
||||||
attributes.push(("type", r#type.clone()));
|
|
||||||
}
|
|
||||||
if let Some(blocking) = &self.blocking {
|
|
||||||
attributes.push(("blocking", blocking.clone()));
|
|
||||||
}
|
|
||||||
attributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a [`link`](crate::elements::link) tag into the head of the page.
|
|
||||||
///
|
|
||||||
/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
|
|
||||||
/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// fn RedBackground() -> Element {
|
|
||||||
/// rsx! {
|
|
||||||
/// // You can use the meta component to render a meta tag into the head of the page
|
|
||||||
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
|
||||||
/// head::Link {
|
|
||||||
/// href: asset!("./assets/style.css"),
|
|
||||||
/// rel: "stylesheet",
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// <div class="warning">
|
|
||||||
///
|
|
||||||
/// Any updates to the props after the first render will not be reflected in the head.
|
|
||||||
///
|
|
||||||
/// </div>
|
|
||||||
#[doc(alias = "<link>")]
|
|
||||||
#[component]
|
|
||||||
pub fn Link(props: LinkProps) -> Element {
|
|
||||||
use_update_warning(&props, "Link {}");
|
|
||||||
|
|
||||||
use_hook(|| {
|
|
||||||
if let Some(href) = &props.href {
|
|
||||||
if !should_insert_link(href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let document = document();
|
|
||||||
document.create_link(props);
|
|
||||||
});
|
|
||||||
|
|
||||||
VNode::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
|
|
||||||
match ScopeId::ROOT.has_context::<T>() {
|
|
||||||
Some(context) => context,
|
|
||||||
None => {
|
|
||||||
let context = T::default();
|
|
||||||
ScopeId::ROOT.provide_context(context.clone());
|
|
||||||
context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct LinkContext(DeduplicationContext);
|
|
||||||
|
|
||||||
fn should_insert_link(href: &str) -> bool {
|
|
||||||
get_or_insert_root_context::<LinkContext>()
|
|
||||||
.0
|
|
||||||
.should_insert(href)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct ScriptContext(DeduplicationContext);
|
|
||||||
|
|
||||||
fn should_insert_script(src: &str) -> bool {
|
|
||||||
get_or_insert_root_context::<ScriptContext>()
|
|
||||||
.0
|
|
||||||
.should_insert(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct StyleContext(DeduplicationContext);
|
|
||||||
|
|
||||||
fn should_insert_style(href: &str) -> bool {
|
|
||||||
get_or_insert_root_context::<StyleContext>()
|
|
||||||
.0
|
|
||||||
.should_insert(href)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
|
|
||||||
|
|
||||||
impl DeduplicationContext {
|
|
||||||
fn should_insert(&self, href: &str) -> bool {
|
|
||||||
let mut set = self.0.borrow_mut();
|
|
||||||
let present = set.contains(href);
|
|
||||||
if !present {
|
|
||||||
set.insert(href.to_string());
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
// API inspired by Reacts implementation of head only elements. We use components here instead of elements to simplify internals.
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
rc::Rc,
|
|
||||||
task::{Context, Poll},
|
|
||||||
};
|
|
||||||
|
|
||||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
mod eval;
|
|
||||||
pub use eval::*;
|
|
||||||
|
|
||||||
pub mod head;
|
|
||||||
pub use head::{Meta, MetaProps, Script, ScriptProps, Style, StyleProps, Title, TitleProps};
|
|
||||||
|
|
||||||
fn format_string_for_js(s: &str) -> String {
|
|
||||||
let escaped = s
|
|
||||||
.replace('\\', "\\\\")
|
|
||||||
.replace('\n', "\\n")
|
|
||||||
.replace('\r', "\\r")
|
|
||||||
.replace('"', "\\\"");
|
|
||||||
format!("\"{escaped}\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_attributes(attributes: &[(&str, String)]) -> String {
|
|
||||||
let mut formatted = String::from("[");
|
|
||||||
for (key, value) in attributes {
|
|
||||||
formatted.push_str(&format!(
|
|
||||||
"[{}, {}],",
|
|
||||||
format_string_for_js(key),
|
|
||||||
format_string_for_js(value)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if formatted.ends_with(',') {
|
|
||||||
formatted.pop();
|
|
||||||
}
|
|
||||||
formatted.push(']');
|
|
||||||
formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_element_in_head(
|
|
||||||
tag: &str,
|
|
||||||
attributes: &[(&str, String)],
|
|
||||||
children: Option<String>,
|
|
||||||
) -> String {
|
|
||||||
let helpers = include_str!("../js/head.js");
|
|
||||||
let attributes = format_attributes(attributes);
|
|
||||||
let children = children
|
|
||||||
.as_deref()
|
|
||||||
.map(format_string_for_js)
|
|
||||||
.unwrap_or("null".to_string());
|
|
||||||
let tag = format_string_for_js(tag);
|
|
||||||
format!(r#"{helpers};window.createElementInHead({tag}, {attributes}, {children});"#)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A provider for document-related functionality. By default most methods are driven through [`eval`].
|
|
||||||
pub trait Document {
|
|
||||||
/// Create a new evaluator for the document that evaluates JavaScript and facilitates communication between JavaScript and Rust.
|
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>>;
|
|
||||||
|
|
||||||
/// Set the title of the document
|
|
||||||
fn set_title(&self, title: String) {
|
|
||||||
self.new_evaluator(format!("document.title = {title:?};"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new meta tag
|
|
||||||
fn create_meta(&self, props: MetaProps) {
|
|
||||||
let attributes = props.attributes();
|
|
||||||
let js = create_element_in_head("meta", &attributes, None);
|
|
||||||
self.new_evaluator(js);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new script tag
|
|
||||||
fn create_script(&self, props: ScriptProps) {
|
|
||||||
let attributes = props.attributes();
|
|
||||||
let js = match (&props.src, props.script_contents()) {
|
|
||||||
// The script has inline contents, render it as a script tag
|
|
||||||
(_, Ok(contents)) => create_element_in_head("script", &attributes, Some(contents)),
|
|
||||||
// The script has a src, render it as a script tag without a body
|
|
||||||
(Some(_), _) => create_element_in_head("script", &attributes, None),
|
|
||||||
// The script has neither contents nor src, log an error
|
|
||||||
(None, Err(err)) => {
|
|
||||||
err.log("Script");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.new_evaluator(js);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new style tag
|
|
||||||
fn create_style(&self, props: StyleProps) {
|
|
||||||
let mut attributes = props.attributes();
|
|
||||||
let js = match (&props.href, props.style_contents()) {
|
|
||||||
// The style has inline contents, render it as a style tag
|
|
||||||
(_, Ok(contents)) => create_element_in_head("style", &attributes, Some(contents)),
|
|
||||||
// The style has a src, render it as a link tag
|
|
||||||
(Some(_), _) => {
|
|
||||||
attributes.push(("type", "text/css".into()));
|
|
||||||
create_element_in_head("link", &attributes, None)
|
|
||||||
}
|
|
||||||
// The style has neither contents nor src, log an error
|
|
||||||
(None, Err(err)) => {
|
|
||||||
err.log("Style");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.new_evaluator(js);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new link tag
|
|
||||||
fn create_link(&self, props: head::LinkProps) {
|
|
||||||
let attributes = props.attributes();
|
|
||||||
let js = create_element_in_head("link", &attributes, None);
|
|
||||||
self.new_evaluator(js);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a reference to the document as `dyn Any`
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default No-Op document
|
|
||||||
pub struct NoOpDocument;
|
|
||||||
|
|
||||||
impl Document for NoOpDocument {
|
|
||||||
fn new_evaluator(&self, _js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
|
||||||
tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle.");
|
|
||||||
UnsyncStorage::owner().insert(Box::new(NoOpEvaluator))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default No-Op evaluator
|
|
||||||
pub struct NoOpEvaluator;
|
|
||||||
impl Evaluator for NoOpEvaluator {
|
|
||||||
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
|
|
||||||
Err(EvalError::Unsupported)
|
|
||||||
}
|
|
||||||
fn poll_recv(
|
|
||||||
&mut self,
|
|
||||||
_context: &mut Context<'_>,
|
|
||||||
) -> Poll<Result<serde_json::Value, EvalError>> {
|
|
||||||
Poll::Ready(Err(EvalError::Unsupported))
|
|
||||||
}
|
|
||||||
fn poll_join(
|
|
||||||
&mut self,
|
|
||||||
_context: &mut Context<'_>,
|
|
||||||
) -> Poll<Result<serde_json::Value, EvalError>> {
|
|
||||||
Poll::Ready(Err(EvalError::Unsupported))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the document provider for the current platform or a no-op provider if the platform doesn't document functionality.
|
|
||||||
pub fn document() -> Rc<dyn Document> {
|
|
||||||
dioxus_core::prelude::try_consume_context::<Rc<dyn Document>>()
|
|
||||||
// Create a NoOp provider that always logs an error when trying to evaluate
|
|
||||||
// That way, we can still compile and run the code without a real provider
|
|
||||||
.unwrap_or_else(|| Rc::new(NoOpDocument) as Rc<dyn Document>)
|
|
||||||
}
|
|
|
@ -41,9 +41,6 @@ pub use elements::*;
|
||||||
pub use events::*;
|
pub use events::*;
|
||||||
pub use render_template::*;
|
pub use render_template::*;
|
||||||
|
|
||||||
#[cfg(feature = "document")]
|
|
||||||
pub mod document;
|
|
||||||
|
|
||||||
pub mod extensions {
|
pub mod extensions {
|
||||||
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
|
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
|
||||||
pub use crate::elements::extensions::*;
|
pub use crate::elements::extensions::*;
|
||||||
|
@ -51,11 +48,6 @@ pub mod extensions {
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
|
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
|
||||||
#[cfg(feature = "document")]
|
|
||||||
pub use crate::document::{
|
|
||||||
self, document, eval, head, Document, Meta, MetaProps, Script, ScriptProps, Style,
|
|
||||||
StyleProps, Title, TitleProps, UseEval,
|
|
||||||
};
|
|
||||||
pub use crate::elements::extensions::*;
|
pub use crate::elements::extensions::*;
|
||||||
pub use crate::events::*;
|
pub use crate::events::*;
|
||||||
pub use crate::point_interaction::*;
|
pub use crate::point_interaction::*;
|
||||||
|
|
|
@ -22,7 +22,8 @@ tokio-stream = { version = "0.1.11", features = ["net"] }
|
||||||
tokio-util = { version = "0.7.4", features = ["rt"] }
|
tokio-util = { version = "0.7.4", features = ["rt"] }
|
||||||
serde = { version = "1.0.151", features = ["derive"] }
|
serde = { version = "1.0.151", features = ["derive"] }
|
||||||
serde_json = "1.0.91"
|
serde_json = "1.0.91"
|
||||||
dioxus-html = { workspace = true, features = ["serialize", "document", "mounted"] }
|
dioxus-html = { workspace = true, features = ["serialize", "mounted"] }
|
||||||
|
dioxus-document = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use dioxus_core::ScopeId;
|
use dioxus_core::ScopeId;
|
||||||
use dioxus_html::document::{Document, EvalError, Evaluator};
|
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
||||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
@ -18,12 +18,8 @@ pub struct LiveviewDocument {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document for LiveviewDocument {
|
impl Document for LiveviewDocument {
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn eval(&self, js: String) -> Eval {
|
||||||
LiveviewEvaluator::create(self.query.clone(), js)
|
Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ fn app() -> Element {
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "hello axum! {count}" }
|
h1 { "hello axum! {count}" }
|
||||||
Title { "hello axum! {count}" }
|
document::Title { "hello axum! {count}" }
|
||||||
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
|
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
|
||||||
button {
|
button {
|
||||||
class: "server-button",
|
class: "server-button",
|
||||||
|
|
|
@ -44,7 +44,7 @@ fn LoadTitle() -> Element {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
Title { "{title.title}" }
|
document::Title { "{title.title}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ fn app() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
"hello axum! {num}"
|
"hello axum! {num}"
|
||||||
Title { "hello axum! {num}" }
|
document::Title { "hello axum! {num}" }
|
||||||
button { class: "increment-button", onclick: move |_| num += 1, "Increment" }
|
button { class: "increment-button", onclick: move |_| num += 1, "Increment" }
|
||||||
}
|
}
|
||||||
svg { circle { cx: 50, cy: 50, r: 40, stroke: "green", fill: "yellow" } }
|
svg { circle { cx: 50, cy: 50, r: 40, stroke: "green", fill: "yellow" } }
|
||||||
|
@ -24,7 +24,7 @@ fn app() -> Element {
|
||||||
button {
|
button {
|
||||||
class: "eval-button",
|
class: "eval-button",
|
||||||
onclick: move |_| async move {
|
onclick: move |_| async move {
|
||||||
let mut eval = eval(
|
let mut eval = document::eval(
|
||||||
r#"
|
r#"
|
||||||
window.document.title = 'Hello from Dioxus Eval!';
|
window.document.title = 'Hello from Dioxus Eval!';
|
||||||
// Receive and multiply 10 numbers
|
// Receive and multiply 10 numbers
|
||||||
|
@ -38,9 +38,9 @@ fn app() -> Element {
|
||||||
|
|
||||||
// Send 10 numbers
|
// Send 10 numbers
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
eval.send(serde_json::Value::from(i)).unwrap();
|
eval.send(i).unwrap();
|
||||||
let value = eval.recv().await.unwrap();
|
let value: i32 = eval.recv().await.unwrap();
|
||||||
assert_eq!(value, serde_json::Value::from(i * 2));
|
assert_eq!(value, i * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = eval.recv().await;
|
let result = eval.recv().await;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::HistoryProvider;
|
use super::HistoryProvider;
|
||||||
use crate::routable::Routable;
|
use crate::routable::Routable;
|
||||||
|
use dioxus_lib::document::Eval;
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
use document::UseEval;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::{Mutex, RwLock};
|
use std::sync::{Mutex, RwLock};
|
||||||
use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
|
use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
|
||||||
|
@ -168,11 +168,10 @@ where
|
||||||
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
||||||
Arc::new(RwLock::new(Arc::new(|| {})));
|
Arc::new(RwLock::new(Arc::new(|| {})));
|
||||||
|
|
||||||
let eval_provider = document();
|
let eval_provider = dioxus_lib::document::document();
|
||||||
|
|
||||||
let create_eval = Rc::new(move |script: &str| {
|
let create_eval = Rc::new(move |script: &str| eval_provider.eval(script.to_string()))
|
||||||
UseEval::new(eval_provider.new_evaluator(script.to_string()))
|
as Rc<dyn Fn(&str) -> Eval>;
|
||||||
}) as Rc<dyn Fn(&str) -> UseEval>;
|
|
||||||
|
|
||||||
// Listen to server actions
|
// Listen to server actions
|
||||||
spawn({
|
spawn({
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use dioxus_isrg::*;
|
use dioxus_isrg::*;
|
||||||
|
use dioxus_lib::document::Document;
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
use dioxus_router::prelude::*;
|
use dioxus_router::prelude::*;
|
||||||
use dioxus_ssr::renderer;
|
use dioxus_ssr::renderer;
|
||||||
|
|
|
@ -13,6 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-core-types = { workspace = true }
|
dioxus-core-types = { workspace = true }
|
||||||
dioxus-html = { workspace = true }
|
dioxus-html = { workspace = true }
|
||||||
|
dioxus-document = { workspace = true }
|
||||||
dioxus-devtools = { workspace = true }
|
dioxus-devtools = { workspace = true }
|
||||||
dioxus-signals = { workspace = true }
|
dioxus-signals = { workspace = true }
|
||||||
dioxus-interpreter-js = { workspace = true, features = [
|
dioxus-interpreter-js = { workspace = true, features = [
|
||||||
|
@ -100,7 +101,7 @@ file_engine = [
|
||||||
"web-sys/FileReader"
|
"web-sys/FileReader"
|
||||||
]
|
]
|
||||||
devtools = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location", "dep:serde_json", "dep:serde", "dioxus-core/serialize"]
|
devtools = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location", "dep:serde_json", "dep:serde", "dioxus-core/serialize"]
|
||||||
document = ["dioxus-html/document", "dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"]
|
document = ["dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus = { workspace = true, default-features = true }
|
dioxus = { workspace = true, default-features = true }
|
||||||
|
|
|
@ -8,7 +8,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use dioxus_core::ScopeId;
|
use dioxus_core::ScopeId;
|
||||||
use dioxus_devtools::{DevserverMsg, HotReloadMsg};
|
use dioxus_devtools::{DevserverMsg, HotReloadMsg};
|
||||||
use dioxus_html::prelude::eval;
|
use dioxus_document::eval;
|
||||||
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
|
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
|
||||||
use js_sys::JsString;
|
use js_sys::JsString;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use dioxus_core::ScopeId;
|
use dioxus_core::ScopeId;
|
||||||
use dioxus_html::document::{Document, EvalError, Evaluator, NoOpEvaluator};
|
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
||||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
||||||
use js_sys::Function;
|
use js_sys::Function;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -64,12 +64,8 @@ pub fn init_document() {
|
||||||
/// The web-target's document provider.
|
/// The web-target's document provider.
|
||||||
pub struct WebDocument;
|
pub struct WebDocument;
|
||||||
impl Document for WebDocument {
|
impl Document for WebDocument {
|
||||||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn eval(&self, js: String) -> Eval {
|
||||||
WebEvaluator::create(js)
|
Eval::new(WebEvaluator::create(js))
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,10 +91,8 @@ impl WebEvaluator {
|
||||||
fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||||
let owner = UnsyncStorage::owner();
|
let owner = UnsyncStorage::owner();
|
||||||
|
|
||||||
let generational_box = owner.insert(Box::new(NoOpEvaluator) as Box<dyn Evaluator>);
|
|
||||||
|
|
||||||
// add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
|
// add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
|
||||||
let channels = WebDioxusChannel::new(JSOwner::new(owner));
|
let channels = WebDioxusChannel::new(JSOwner::new(owner.clone()));
|
||||||
|
|
||||||
// The Rust side of the channel is a weak reference to the DioxusChannel
|
// The Rust side of the channel is a weak reference to the DioxusChannel
|
||||||
let weak_channels = channels.weak();
|
let weak_channels = channels.weak();
|
||||||
|
@ -131,13 +125,11 @@ impl WebEvaluator {
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
|
|
||||||
generational_box.set(Box::new(Self {
|
owner.insert(Box::new(Self {
|
||||||
channels: weak_channels,
|
channels: weak_channels,
|
||||||
result: Some(result),
|
result: Some(result),
|
||||||
next_future: None,
|
next_future: None,
|
||||||
}) as Box<dyn Evaluator>);
|
}) as Box<dyn Evaluator>)
|
||||||
|
|
||||||
generational_box
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
[3479327739946104450]
|
[1614426347475783279]
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
DioxusChannel,
|
DioxusChannel,
|
||||||
Channel,
|
Channel,
|
||||||
WeakDioxusChannel,
|
WeakDioxusChannel,
|
||||||
} from "../../../html/src/ts/eval";
|
} from "../../../document/src/ts/eval";
|
||||||
|
|
||||||
export class WebDioxusChannel extends DioxusChannel {
|
export class WebDioxusChannel extends DioxusChannel {
|
||||||
js_to_rust: Channel;
|
js_to_rust: Channel;
|
||||||
|
|
Loading…
Reference in a new issue