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:
Evan Almloff 2024-10-14 12:33:37 -05:00 committed by GitHub
parent 2b219c826c
commit 519ec9d294
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 1113 additions and 1044 deletions

23
Cargo.lock generated
View file

@ -2380,6 +2380,7 @@ dependencies = [
"dioxus-core-macro",
"dioxus-desktop",
"dioxus-devtools",
"dioxus-document",
"dioxus-fullstack",
"dioxus-hooks",
"dioxus-html",
@ -2579,6 +2580,7 @@ dependencies = [
"dioxus-cli-config",
"dioxus-core",
"dioxus-devtools",
"dioxus-document",
"dioxus-hooks",
"dioxus-html",
"dioxus-interpreter-js",
@ -2638,6 +2640,24 @@ dependencies = [
"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]]
name = "dioxus-examples"
version = "0.6.0-alpha.2"
@ -2815,6 +2835,7 @@ dependencies = [
"dioxus-config-macro",
"dioxus-core",
"dioxus-core-macro",
"dioxus-document",
"dioxus-hooks",
"dioxus-html",
"dioxus-rsx",
@ -2830,6 +2851,7 @@ dependencies = [
"dioxus-cli-config",
"dioxus-core",
"dioxus-devtools",
"dioxus-document",
"dioxus-html",
"dioxus-interpreter-js",
"futures-channel",
@ -3074,6 +3096,7 @@ dependencies = [
"dioxus-core",
"dioxus-core-types",
"dioxus-devtools",
"dioxus-document",
"dioxus-html",
"dioxus-interpreter-js",
"dioxus-signals",

View file

@ -1,39 +1,40 @@
[workspace]
resolver = "2"
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/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-rosetta",
"packages/generational-box",
"packages/signals",
"packages/devtools",
"packages/devtools-types",
"packages/fullstack",
"packages/rsx",
"packages/server-macro",
"packages/signals",
"packages/ssr",
"packages/static-generation",
"packages/lazy-js-bundle",
"packages/web",
# Full project examples
"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-router = { path = "packages/router", 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-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.0" }
dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.0" }

View file

@ -3,7 +3,7 @@ use dioxus::prelude::*;
#[component]
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
head::Link {
document::Link {
rel: "stylesheet",
href: asset!("./public/loading.css")
}

View file

@ -17,7 +17,7 @@ mod api;
fn main() {
dioxus::launch(|| {
rsx! {
head::Link {
document::Link {
rel: "stylesheet",
href: asset!("./public/tailwind.css")
}

View file

@ -21,12 +21,12 @@ fn app() -> Element {
let mut files = use_signal(Files::new);
rsx! {
head::Link {
document::Link {
rel: "stylesheet",
href: asset!("./assets/fileexplorer.css")
}
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 {
i { class: "material-icons icon-menu", "menu" }
h1 { "Files: " {files.read().current()} }

View file

@ -36,7 +36,7 @@ pub fn App() -> Element {
#[component]
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
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 {
width: "50%",

View file

@ -26,7 +26,7 @@ fn app() -> Element {
};
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
div { id: "container",
// focusing is necessary to catch keyboard events
div { id: "receiver", tabindex: 0,

View file

@ -54,7 +54,7 @@ fn app() -> Element {
};
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
div { id: "wrapper",
div { class: "app",
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,

View file

@ -29,7 +29,7 @@ fn app() -> Element {
let mut state = use_signal(Calculator::new);
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 { class: "app",
div {

View file

@ -36,7 +36,7 @@ fn app() -> Element {
);
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
div { id: "app",
div { id: "title", "Carpe diem 🎉" }
div { id: "clock-display", "{time}" }

View file

@ -40,7 +40,7 @@ fn app() -> Element {
});
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
h1 { "Input Roulette" }
button { onclick: move |_| running.toggle(), "Toggle roulette" }
div { id: "roulette-grid",

View file

@ -16,7 +16,7 @@ fn app() -> Element {
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
div { id: "controls",
button { onclick: move |_| counters.write().push(0), "Add counter" }

View file

@ -20,13 +20,13 @@ fn main() {
}))
.launch(|| {
rsx! {
head::Link {
document::Link {
rel: "stylesheet",
href: asset!("https://unpkg.com/purecss@2.0.6/build/pure-min.css"),
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
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" }
Router::<Route> {}
}

View file

@ -24,7 +24,7 @@ fn app() -> Element {
});
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
h1 { "Dynamic Assets" }
img { src: "/logos/logo.png" }
}

View file

@ -19,7 +19,7 @@ fn app() -> Element {
// 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()`
// 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#"
dioxus.send("Hi from JS!");
let msg = await dioxus.recv();
@ -29,10 +29,10 @@ fn app() -> Element {
);
// 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".
let res = eval.recv().await.unwrap();
let res: String = eval.recv().await.unwrap();
// This will print "Hi from JS!" and "Hi from Rust!".
println!("{:?}", eval.await);

View file

@ -43,7 +43,7 @@ fn app() -> Element {
};
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
h1 { "File Upload Example" }
p { "Drop a .txt, .rs, or .js file here to read it" }

View file

@ -14,7 +14,7 @@ const STYLE: &str = asset!("./examples/assets/flat_router.css");
fn main() {
dioxus::launch(|| {
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
Router::<Route> {}
}
})

View file

@ -18,7 +18,7 @@ fn main() {
fn app() -> Element {
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
Increment {}
Decrement {}
Reset {}

View file

@ -36,7 +36,7 @@ fn app() -> Element {
});
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: "columns",
div { class: "column",

View file

@ -16,7 +16,7 @@ fn main() {
fn app() -> Element {
rsx! (
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
Router::<Route> {}
)
}

View file

@ -12,23 +12,23 @@ fn app() -> Element {
// 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
// This example sets up meta tags for the open graph protocol for social media previews
Meta {
document::Meta {
property: "og:title",
content: "My Site",
}
Meta {
document::Meta {
property: "og:type",
content: "website",
}
Meta {
document::Meta {
property: "og:url",
content: "https://www.example.com",
}
Meta {
document::Meta {
property: "og:image",
content: "https://example.com/image.jpg",
}
Meta {
document::Meta {
name: "description",
content: "My Site is a site",
}

View file

@ -22,7 +22,7 @@ fn app() -> Element {
_ = use_global_shortcut("cmd+g", move || show_overlay.toggle());
rsx! {
head::Link {
document::Link {
rel: "stylesheet",
href: asset!("./examples/assets/overlay.css"),
}

View file

@ -28,7 +28,7 @@ fn app() -> Element {
};
rsx!(
head::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
div {
width: "50%",
height: "50%",

View file

@ -17,7 +17,7 @@ fn app() -> Element {
let mut state = use_signal(|| PlayerState { is_playing: false });
rsx!(
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
h1 {"Select an option"}
// Add some cute animations if the radio is playing!

View file

@ -15,7 +15,7 @@ fn app() -> Element {
let mut dimensions = use_signal(Size2D::zero);
rsx!(
head::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
document::Link { rel: "stylesheet", href: asset!("./examples/assets/read_size.css") }
div {
width: "50%",
height: "50%",

View file

@ -13,7 +13,7 @@ const STYLE: &str = asset!("./examples/assets/router.css");
fn main() {
dioxus::launch(|| {
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
Router::<Route> {}
}
});

View file

@ -14,7 +14,7 @@ fn app() -> Element {
div {
// 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
Title { "My Application (Count {count})" }
document::Title { "My Application (Count {count})" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}

View file

@ -63,7 +63,7 @@ fn app() -> Element {
};
rsx! {
head::Link { rel: "stylesheet", href: STYLE }
document::Link { rel: "stylesheet", href: STYLE }
section { class: "todoapp",
TodoHeader { todos }
section { class: "main",

View file

@ -19,7 +19,7 @@ fn app() -> Element {
let current_weather = use_resource(move || async move { get_weather(&country()).await });
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: "flex items-center justify-center flex-row",
div { class: "flex items-start justify-center flex-row",

View file

@ -26,7 +26,7 @@ fn main() {
fn app() -> Element {
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 {}
div { class: "container mx-auto",
div { class: "grid grid-cols-5",

View file

@ -156,7 +156,7 @@ impl BuildRequest {
};
match variant {
ResourceType::Style => format!(
" head::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
" document::Link {{ rel: \"stylesheet\", href: asset!(css(\"{}\")) }}",
path.display()
),
ResourceType::Script => {

View file

@ -32,7 +32,7 @@
//! use_effect(move || {
//! let id = id.read();
//! // 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! {

View file

@ -517,7 +517,7 @@ fn nested_suspense_resolves_client() {
let title = use_resource(move || async_content(0)).suspend()?();
rsx! {
Title { "{title.title}" }
document::Title { "{title.title}" }
}
}

View file

@ -15,8 +15,8 @@ dioxus-html = { workspace = true, features = [
"serialize",
"mounted",
"file_engine",
"document",
] }
dioxus-document = { workspace = true }
dioxus-signals = { workspace = true, optional = true }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
dioxus-cli-config = { workspace = true }

View file

@ -16,23 +16,23 @@ static EVALS_RETURNED: GlobalSignal<usize> = Signal::global(|| 0);
fn app() -> Element {
// Double 100 values in the value
use_future(|| async {
let mut eval = eval(
let mut eval = document::eval(
r#"for (let i = 0; i < 100; i++) {
let value = await dioxus.recv();
dioxus.send(value*2);
}"#,
);
for i in 0..100 {
eval.send(serde_json::Value::from(i)).unwrap();
let value = eval.recv().await.unwrap();
assert_eq!(value, serde_json::Value::from(i * 2));
eval.send(i).unwrap();
let value: i32 = eval.recv().await.unwrap();
assert_eq!(value, i * 2);
EVALS_RECEIVED.with_mut(|x| *x += 1);
}
});
// Make sure returning no value resolves the future
use_future(|| async {
let eval = eval(r#"return;"#);
let eval = document::eval(r#"return;"#);
eval.await.unwrap();
EVALS_RETURNED.with_mut(|x| *x += 1);
@ -40,7 +40,7 @@ fn app() -> Element {
// Return a value from the future
use_future(|| async {
let eval = eval(
let eval = document::eval(
r#"
return [1, 2, 3];
"#,

View file

@ -16,7 +16,7 @@ fn use_inner_html(id: &'static str) -> Option<String> {
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let res = eval(&format!(
let res = document::eval(&format!(
r#"let element = document.getElementById('{}');
return element.innerHTML"#,
id

View file

@ -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();
});
})
}

View file

@ -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 crate::{query::Query, DesktopContext};
@ -18,17 +18,13 @@ impl DesktopDocument {
}
impl Document for DesktopDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
DesktopEvaluator::create(self.desktop_ctx.clone(), js)
fn eval(&self, js: String) -> Eval {
Eval::new(DesktopEvaluator::create(self.desktop_ctx.clone(), js))
}
fn set_title(&self, title: String) {
self.desktop_ctx.window.set_title(&title);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
/// Represents a desktop-target's JavaScript evaluator.

View file

@ -1 +1 @@
[11927251734412729446]
[14101548031762241351]

View file

@ -2,7 +2,7 @@ import {
Channel,
DioxusChannel,
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
declare global {

View file

@ -13,8 +13,9 @@ use crate::{
Config, DesktopContext, DesktopService,
};
use dioxus_core::{Runtime, ScopeId, VirtualDom};
use dioxus_document::Document;
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 std::cell::OnceCell;
use std::sync::Arc;

View file

@ -13,6 +13,7 @@ rust-version = "1.79.0"
[dependencies]
dioxus-core = { workspace = true }
dioxus-html = { workspace = true, optional = true }
dioxus-document = { workspace = true, optional = true }
dioxus-core-macro = { workspace = true, optional = true }
dioxus-config-macro = { workspace = true, optional = true }
dioxus-hooks = { workspace = true, optional = true }
@ -26,7 +27,7 @@ dioxus = { workspace = true }
default = ["macro", "html", "signals", "hooks"]
signals = ["dep:dioxus-signals"]
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"]
[package.metadata.docs.rs]

View file

@ -16,6 +16,9 @@ pub mod events {
#[cfg(feature = "html")]
pub use dioxus_html as html;
#[cfg(feature = "html")]
pub use dioxus_document as document;
#[cfg(feature = "macro")]
pub use dioxus_rsx as rsx;

View file

@ -13,6 +13,7 @@ rust-version = "1.79.0"
[dependencies]
dioxus-core = { workspace = true }
dioxus-html = { workspace = true, default-features = false, optional = true }
dioxus-document = { workspace = true, optional = true }
dioxus-core-macro = { workspace = true, optional = true }
dioxus-config-macro = { 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"]
file_engine = ["dioxus-web?/file_engine"]
asset = ["dep:manganis", "dioxus-core/manganis"]
document = ["dioxus-web?/document", "dioxus-html?/document"]
document = ["dioxus-web?/document", "dioxus-document"]
launch = ["dep:dioxus-config-macro"]
router = ["dep:dioxus-router"]

View file

@ -330,7 +330,7 @@ fn web_launch(
#[cfg(all(feature = "static-generation", not(feature = "fullstack")))]
use dioxus_static_site_generation::document;
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

View file

@ -50,6 +50,10 @@ pub mod events {
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_attr(docsrs, doc(cfg(feature = "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 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_attr(docsrs, doc(cfg(feature = "hooks")))]

View 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 }

View file

@ -0,0 +1 @@
// this script is included as an asset!() for a test

View file

@ -0,0 +1 @@
/* this stylesheet is included as an asset!() for a test */

View file

@ -1,6 +1,6 @@
# 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">
@ -18,7 +18,7 @@ fn App() -> Element {
button {
onclick: move |_| async move {
// 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";"#);
// 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
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
use dioxus::prelude::*;
@ -43,7 +43,7 @@ fn app() -> Element {
onclick: move |_| {
// You can pass initial data to the eval function by formatting it into the JavaScript code.
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.
let value = await dioxus.recv();
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`.
for i in 0..LOOP_COUNT {
eval.send(i.into()).unwrap();
eval.send(i).unwrap();
}
},
"Log Count"
@ -62,7 +62,7 @@ fn app() -> Element {
## 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
use dioxus::prelude::*;
@ -72,14 +72,14 @@ fn app() -> Element {
button {
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`.
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.
dioxus.send(i);
}"#);
// You can receive values from the JavaScript code with the `recv` method on the object returned by `eval`.
for _ in 0..10 {
let value = eval.recv().await.unwrap();
let value: i32 = eval.recv().await.unwrap();
println!("Received {}", value);
}
},
@ -104,12 +104,12 @@ const SCRIPT: &str = r#"
fn app() -> Element {
// ❌ 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
use_effect(move || {
spawn(async {
let count = eval(SCRIPT).await;
let count = document::eval(SCRIPT).await;
println!("Count is {:?}", count);
});
});

View file

@ -4,7 +4,7 @@ Dioxus includes a series of components that render into the head of the page:
- [Title](crate::Title)
- [Meta](crate::Meta)
- [head::Link](crate::head::Link)
- [document::Link](crate::document::Link)
- [Script](crate::Script)
- [Style](crate::Style)
@ -25,7 +25,7 @@ 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 {
document::Meta {
http_equiv: "refresh",
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 {
rsx! {
// This will render in SSR
Title { "My Page" }
document::Title { "My Page" }
SuspenseBoundary {
fallback: |_| rsx! { "Loading..." },
LoadData {
// 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" }
}
}
}

View 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))
}
}

View 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)
}

View 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()
}

View 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
}
}
}

View 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)
}

View 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)
}

View 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()
}

View 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 {}

View 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>>;
}

View 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())
}

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "CommonJS",
"lib": [
"ES2015",
"DOM",
"dom",
"dom.iterable",
"ESNext"
],
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
},
"exclude": [
"**/*.spec.ts"
]
}

View file

@ -4,9 +4,8 @@
use std::cell::RefCell;
use dioxus_lib::{html::document::*, prelude::*};
use dioxus_lib::{document::*, prelude::*};
use dioxus_ssr::Renderer;
use generational_box::GenerationalBox;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
@ -71,8 +70,8 @@ impl ServerDocument {
}
impl Document for ServerDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
NoOpDocument.new_evaluator(js)
fn eval(&self, js: String) -> Eval {
NoOpDocument.eval(js)
}
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.serialize_for_hydration();
self.0.borrow_mut().link.push(rsx! {
@ -173,8 +172,4 @@ impl Document for ServerDocument {
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View file

@ -1,7 +1,7 @@
#![allow(unused)]
//! 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;
fn head_element_written_on_server() -> bool {
@ -15,11 +15,8 @@ fn head_element_written_on_server() -> bool {
pub struct FullstackWebDocument;
impl Document for FullstackWebDocument {
fn new_evaluator(
&self,
js: String,
) -> generational_box::GenerationalBox<Box<dyn dioxus_lib::prelude::document::Evaluator>> {
WebDocument.new_evaluator(js)
fn eval(&self, js: String) -> Eval {
WebDocument.eval(js)
}
fn set_title(&self, title: String) {
@ -29,35 +26,31 @@ impl Document for FullstackWebDocument {
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() {
return;
}
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() {
return;
}
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() {
return;
}
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() {
return;
}
WebDocument.create_link(props);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View file

@ -1,7 +1,9 @@
//! A shared pool of renderers for efficient server side rendering.
use crate::document::ServerDocument;
use crate::streaming::{Mount, StreamingRenderer};
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
use dioxus_isrg::{CachedRender, RenderFreshness};
use dioxus_lib::document::Document;
use dioxus_ssr::Renderer;
use futures_channel::mpsc::Sender;
use futures_util::{Stream, StreamExt};
@ -165,6 +167,7 @@ impl SsrRendererPool {
let join_handle = spawn_platform(move || async move {
let mut virtual_dom = virtual_dom_factory();
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>);
// poll the future, which may call server_context()
@ -431,11 +434,8 @@ impl FullstackHTMLTemplate {
let ServeConfig { index, .. } = &self.cfg;
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());
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
document.and_then(|document| document.title())
};
@ -448,11 +448,8 @@ impl FullstackHTMLTemplate {
}
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());
let document: Option<&crate::document::server::ServerDocument> = document
.as_ref()
.and_then(|document| document.as_any().downcast_ref());
if let Some(document) = document {
// Collect any head content from the document provider and inject that into the head
document.render(to)?;

View file

@ -16,7 +16,7 @@ fn MyComponent() -> Element {
let count = count.read();
// You can use the count value to update the DOM manually
eval(&format!(
document::eval(&format!(
r#"var c = document.getElementById("dioxus-canvas");
var ctx = c.getContext("2d");
ctx.font = "30px Arial";

View file

@ -41,7 +41,7 @@ tokio = { workspace = true, features = ["time"] }
manganis = { workspace = true }
[features]
default = ["serialize", "mounted", "document", "file_engine"]
default = ["serialize", "mounted", "file_engine"]
serialize = [
"dep:serde",
"dep:serde_json",
@ -51,10 +51,6 @@ serialize = [
"dioxus-core/serialize"
]
mounted = []
document = [
"dep:serde",
"dep:serde_json"
]
file_engine = [
"dep:async-trait",
]

View file

@ -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 {}

View file

@ -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
}
}
}

View file

@ -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>)
}

View file

@ -41,9 +41,6 @@ pub use elements::*;
pub use events::*;
pub use render_template::*;
#[cfg(feature = "document")]
pub mod document;
pub mod extensions {
pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
pub use crate::elements::extensions::*;
@ -51,11 +48,6 @@ pub mod extensions {
pub mod prelude {
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::events::*;
pub use crate::point_interaction::*;

View file

@ -22,7 +22,8 @@ tokio-stream = { version = "0.1.11", features = ["net"] }
tokio-util = { version = "0.7.4", features = ["rt"] }
serde = { version = "1.0.151", features = ["derive"] }
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 }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }

View file

@ -1,5 +1,5 @@
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 std::rc::Rc;
@ -18,12 +18,8 @@ pub struct LiveviewDocument {
}
impl Document for LiveviewDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
LiveviewEvaluator::create(self.query.clone(), js)
}
fn as_any(&self) -> &dyn std::any::Any {
self
fn eval(&self, js: String) -> Eval {
Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
}
}

View file

@ -17,7 +17,7 @@ fn app() -> Element {
rsx! {
h1 { "hello axum! {count}" }
Title { "hello axum! {count}" }
document::Title { "hello axum! {count}" }
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
button {
class: "server-button",

View file

@ -44,7 +44,7 @@ fn LoadTitle() -> Element {
.unwrap();
rsx! {
Title { "{title.title}" }
document::Title { "{title.title}" }
}
}

View file

@ -9,7 +9,7 @@ fn app() -> Element {
rsx! {
div {
"hello axum! {num}"
Title { "hello axum! {num}" }
document::Title { "hello axum! {num}" }
button { class: "increment-button", onclick: move |_| num += 1, "Increment" }
}
svg { circle { cx: 50, cy: 50, r: 40, stroke: "green", fill: "yellow" } }
@ -24,7 +24,7 @@ fn app() -> Element {
button {
class: "eval-button",
onclick: move |_| async move {
let mut eval = eval(
let mut eval = document::eval(
r#"
window.document.title = 'Hello from Dioxus Eval!';
// Receive and multiply 10 numbers
@ -38,9 +38,9 @@ fn app() -> Element {
// Send 10 numbers
for i in 0..10 {
eval.send(serde_json::Value::from(i)).unwrap();
let value = eval.recv().await.unwrap();
assert_eq!(value, serde_json::Value::from(i * 2));
eval.send(i).unwrap();
let value: i32 = eval.recv().await.unwrap();
assert_eq!(value, i * 2);
}
let result = eval.recv().await;

View file

@ -1,7 +1,7 @@
use super::HistoryProvider;
use crate::routable::Routable;
use dioxus_lib::document::Eval;
use dioxus_lib::prelude::*;
use document::UseEval;
use serde::{Deserialize, Serialize};
use std::sync::{Mutex, RwLock};
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>>> =
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| {
UseEval::new(eval_provider.new_evaluator(script.to_string()))
}) as Rc<dyn Fn(&str) -> UseEval>;
let create_eval = Rc::new(move |script: &str| eval_provider.eval(script.to_string()))
as Rc<dyn Fn(&str) -> Eval>;
// Listen to server actions
spawn({

View file

@ -1,4 +1,5 @@
use dioxus_isrg::*;
use dioxus_lib::document::Document;
use dioxus_lib::prelude::*;
use dioxus_router::prelude::*;
use dioxus_ssr::renderer;

View file

@ -13,6 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
dioxus-core = { workspace = true }
dioxus-core-types = { workspace = true }
dioxus-html = { workspace = true }
dioxus-document = { workspace = true }
dioxus-devtools = { workspace = true }
dioxus-signals = { workspace = true }
dioxus-interpreter-js = { workspace = true, features = [
@ -100,7 +101,7 @@ file_engine = [
"web-sys/FileReader"
]
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]
dioxus = { workspace = true, default-features = true }

View file

@ -8,7 +8,7 @@ use std::time::Duration;
use dioxus_core::ScopeId;
use dioxus_devtools::{DevserverMsg, HotReloadMsg};
use dioxus_html::prelude::eval;
use dioxus_document::eval;
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use js_sys::JsString;
use wasm_bindgen::JsCast;

View file

@ -1,5 +1,5 @@
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 js_sys::Function;
use serde::Serialize;
@ -64,12 +64,8 @@ pub fn init_document() {
/// The web-target's document provider.
pub struct WebDocument;
impl Document for WebDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
WebEvaluator::create(js)
}
fn as_any(&self) -> &dyn std::any::Any {
self
fn eval(&self, js: String) -> Eval {
Eval::new(WebEvaluator::create(js))
}
}
@ -95,10 +91,8 @@ impl WebEvaluator {
fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
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
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
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,
result: Some(result),
next_future: None,
}) as Box<dyn Evaluator>);
generational_box
}) as Box<dyn Evaluator>)
}
}

View file

@ -1 +1 @@
[3479327739946104450]
[1614426347475783279]

View file

@ -2,7 +2,7 @@ import {
DioxusChannel,
Channel,
WeakDioxusChannel,
} from "../../../html/src/ts/eval";
} from "../../../document/src/ts/eval";
export class WebDioxusChannel extends DioxusChannel {
js_to_rust: Channel;