diff --git a/Cargo.lock b/Cargo.lock index c86599f60a..87346a9029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index b408c9d09e..41dceef551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,40 @@ [workspace] resolver = "2" members = [ - "packages/dioxus", - "packages/dioxus-lib", - "packages/core", - "packages/core-types", - "packages/cli", + "packages/autofmt", + "packages/check", "packages/cli-config", - "packages/core-macro", + "packages/cli", "packages/config-macro", - "packages/router-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/router", - "packages/html", - "packages/html-internal-macro", + "packages/fullstack", + "packages/generational-box", "packages/hooks", - "packages/web", - "packages/ssr", - "packages/desktop", - "packages/mobile", + "packages/html-internal-macro", + "packages/html", "packages/interpreter", + "packages/lazy-js-bundle", "packages/liveview", - "packages/autofmt", - "packages/check", - "packages/rsx", + "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" } diff --git a/example-projects/ecommerce-site/src/components/loading.rs b/example-projects/ecommerce-site/src/components/loading.rs index 7718f2185c..cd387ba61c 100644 --- a/example-projects/ecommerce-site/src/components/loading.rs +++ b/example-projects/ecommerce-site/src/components/loading.rs @@ -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") } diff --git a/example-projects/ecommerce-site/src/main.rs b/example-projects/ecommerce-site/src/main.rs index 0ed9b7a467..c6c021d3ae 100644 --- a/example-projects/ecommerce-site/src/main.rs +++ b/example-projects/ecommerce-site/src/main.rs @@ -17,7 +17,7 @@ mod api; fn main() { dioxus::launch(|| { rsx! { - head::Link { + document::Link { rel: "stylesheet", href: asset!("./public/tailwind.css") } diff --git a/example-projects/file-explorer/src/main.rs b/example-projects/file-explorer/src/main.rs index eb769eacee..73c35338bb 100644 --- a/example-projects/file-explorer/src/main.rs +++ b/example-projects/file-explorer/src/main.rs @@ -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()} } diff --git a/example-projects/fullstack-hackernews/src/main.rs b/example-projects/fullstack-hackernews/src/main.rs index d83696a8b3..c36ad3e822 100644 --- a/example-projects/fullstack-hackernews/src/main.rs +++ b/example-projects/fullstack-hackernews/src/main.rs @@ -36,7 +36,7 @@ pub fn App() -> Element { #[component] fn Homepage(story: ReadOnlySignal) -> 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%", diff --git a/examples/all_events.rs b/examples/all_events.rs index 232421ad10..0e572c32f1 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -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, diff --git a/examples/calculator.rs b/examples/calculator.rs index b1761de000..6056e18109 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -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, diff --git a/examples/calculator_mutable.rs b/examples/calculator_mutable.rs index 2984d569da..e5ca06c857 100644 --- a/examples/calculator_mutable.rs +++ b/examples/calculator_mutable.rs @@ -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 { diff --git a/examples/clock.rs b/examples/clock.rs index 4feade1885..678b3c3ab2 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -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}" } diff --git a/examples/control_focus.rs b/examples/control_focus.rs index 0ec425b9f8..9bfdbc3fc3 100644 --- a/examples/control_focus.rs +++ b/examples/control_focus.rs @@ -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", diff --git a/examples/counters.rs b/examples/counters.rs index 0b12cb0ab9..3e8ff3685e 100644 --- a/examples/counters.rs +++ b/examples/counters.rs @@ -16,7 +16,7 @@ fn app() -> Element { let sum = use_memo(move || counters.read().iter().copied().sum::()); 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" } diff --git a/examples/crm.rs b/examples/crm.rs index 641fa415fa..732dd8ee72 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -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:: {} } diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs index 563ec69c6c..6d40f3ada6 100644 --- a/examples/dynamic_asset.rs +++ b/examples/dynamic_asset.rs @@ -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" } } diff --git a/examples/eval.rs b/examples/eval.rs index f232b04a93..d766123f0c 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -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); diff --git a/examples/file_upload.rs b/examples/file_upload.rs index faaefc49f0..1903cf0716 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -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" } diff --git a/examples/flat_router.rs b/examples/flat_router.rs index 94ca7cf065..25dac265bd 100644 --- a/examples/flat_router.rs +++ b/examples/flat_router.rs @@ -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:: {} } }) diff --git a/examples/global.rs b/examples/global.rs index e6a4ade1a0..c2c4e0ce6a 100644 --- a/examples/global.rs +++ b/examples/global.rs @@ -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 {} diff --git a/examples/image_generator_openai.rs b/examples/image_generator_openai.rs index 0a6f69d5bd..600440e7ea 100644 --- a/examples/image_generator_openai.rs +++ b/examples/image_generator_openai.rs @@ -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", diff --git a/examples/link.rs b/examples/link.rs index 776e9f7a51..750dd56af3 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,7 +16,7 @@ fn main() { fn app() -> Element { rsx! ( - head::Link { rel: "stylesheet", href: STYLE } + document::Link { rel: "stylesheet", href: STYLE } Router:: {} ) } diff --git a/examples/meta.rs b/examples/meta.rs index d816ca1832..1030b8354a 100644 --- a/examples/meta.rs +++ b/examples/meta.rs @@ -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", } diff --git a/examples/overlay.rs b/examples/overlay.rs index 7139e96cb4..3cc0f6a306 100644 --- a/examples/overlay.rs +++ b/examples/overlay.rs @@ -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"), } diff --git a/examples/read_size.rs b/examples/read_size.rs index 8ef3ccfe9a..53f3cd1a91 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -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%", diff --git a/examples/reducer.rs b/examples/reducer.rs index e16ae41df5..1398ef49d4 100644 --- a/examples/reducer.rs +++ b/examples/reducer.rs @@ -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! diff --git a/examples/resize.rs b/examples/resize.rs index 48181c2c30..783dec407c 100644 --- a/examples/resize.rs +++ b/examples/resize.rs @@ -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%", diff --git a/examples/router.rs b/examples/router.rs index 951e821119..4a2cc6c5af 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -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:: {} } }); diff --git a/examples/title.rs b/examples/title.rs index c92497492d..b01b38f4e1 100644 --- a/examples/title.rs +++ b/examples/title.rs @@ -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!" } } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 95d27d29c6..8af3ffd9b1 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -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", diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 924cf87eb9..9df13c7a20 100644 --- a/examples/weather_app.rs +++ b/examples/weather_app.rs @@ -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", diff --git a/examples/window_event.rs b/examples/window_event.rs index e2d88cc39a..c5d5dd3e27 100644 --- a/examples/window_event.rs +++ b/examples/window_event.rs @@ -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", diff --git a/packages/cli/src/builder/prepare_html.rs b/packages/cli/src/builder/prepare_html.rs index 7d99dc0e16..fedfa0a2f6 100644 --- a/packages/cli/src/builder/prepare_html.rs +++ b/packages/cli/src/builder/prepare_html.rs @@ -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 => { diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index 2a87586dce..189015832b 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -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! { diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index f2775ab70d..cfabe841fe 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -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}" } } } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 08f281caf1..976c0f3a7d 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -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 } diff --git a/packages/desktop/headless_tests/eval.rs b/packages/desktop/headless_tests/eval.rs index 4e9c9df795..1a348c753d 100644 --- a/packages/desktop/headless_tests/eval.rs +++ b/packages/desktop/headless_tests/eval.rs @@ -16,23 +16,23 @@ static EVALS_RETURNED: GlobalSignal = 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]; "#, diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index ace615a020..cb67e7bec6 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -16,7 +16,7 @@ fn use_inner_html(id: &'static str) -> Option { 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 diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs index 50fac3fb6b..4381f6c756 100644 --- a/packages/desktop/headless_tests/utils.rs +++ b/packages/desktop/headless_tests/utils.rs @@ -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(); }); }) } diff --git a/packages/desktop/src/document.rs b/packages/desktop/src/document.rs index 26c724ff4b..fbc9fbfb2f 100644 --- a/packages/desktop/src/document.rs +++ b/packages/desktop/src/document.rs @@ -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> { - 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. diff --git a/packages/desktop/src/js/hash.txt b/packages/desktop/src/js/hash.txt index 6287f751bc..92bf35c553 100644 --- a/packages/desktop/src/js/hash.txt +++ b/packages/desktop/src/js/hash.txt @@ -1 +1 @@ -[11927251734412729446] \ No newline at end of file +[14101548031762241351] \ No newline at end of file diff --git a/packages/desktop/src/ts/native_eval.ts b/packages/desktop/src/ts/native_eval.ts index 10c4d6aab0..e2880bb4d1 100644 --- a/packages/desktop/src/ts/native_eval.ts +++ b/packages/desktop/src/ts/native_eval.ts @@ -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 { @@ -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 - async rustRecv(): Promise {} + async rustRecv(): Promise { } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 21aa8adbe9..a9cf819bd1 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -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; diff --git a/packages/dioxus-lib/Cargo.toml b/packages/dioxus-lib/Cargo.toml index 86144a9bfe..5ae189e0e0 100644 --- a/packages/dioxus-lib/Cargo.toml +++ b/packages/dioxus-lib/Cargo.toml @@ -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] diff --git a/packages/dioxus-lib/src/lib.rs b/packages/dioxus-lib/src/lib.rs index abf5140d65..5aabbdf6a4 100644 --- a/packages/dioxus-lib/src/lib.rs +++ b/packages/dioxus-lib/src/lib.rs @@ -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; diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 5db3dda5e3..8c8638a8e5 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -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"] diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 7141f5691b..ae7e3984bc 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -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; + as std::rc::Rc; vdom.provide_root_context(document); } vdom diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index beb7b81c5a..43a44dc955 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -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")))] diff --git a/packages/document/Cargo.toml b/packages/document/Cargo.toml new file mode 100644 index 0000000000..76f6f958dd --- /dev/null +++ b/packages/document/Cargo.toml @@ -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 } diff --git a/packages/document/assets/script.js b/packages/document/assets/script.js new file mode 100644 index 0000000000..72714dc658 --- /dev/null +++ b/packages/document/assets/script.js @@ -0,0 +1 @@ +// this script is included as an asset!() for a test diff --git a/packages/document/assets/style.css b/packages/document/assets/style.css new file mode 100644 index 0000000000..b47db6fc63 --- /dev/null +++ b/packages/document/assets/style.css @@ -0,0 +1 @@ +/* this stylesheet is included as an asset!() for a test */ diff --git a/packages/html/build.rs b/packages/document/build.rs similarity index 100% rename from packages/html/build.rs rename to packages/document/build.rs diff --git a/packages/html/docs/eval.md b/packages/document/docs/eval.md similarity index 82% rename from packages/html/docs/eval.md rename to packages/document/docs/eval.md index 673a13f14d..eb7847f162 100644 --- a/packages/html/docs/eval.md +++ b/packages/document/docs/eval.md @@ -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.
@@ -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); }); }); diff --git a/packages/html/docs/head.md b/packages/document/docs/head.md similarity index 91% rename from packages/html/docs/head.md rename to packages/document/docs/head.md index 94dcb99374..b5d022830c 100644 --- a/packages/html/docs/head.md +++ b/packages/document/docs/head.md @@ -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" } } } } diff --git a/packages/document/src/document.rs b/packages/document/src/document.rs new file mode 100644 index 0000000000..709e68638a --- /dev/null +++ b/packages/document/src/document.rs @@ -0,0 +1,156 @@ +use std::sync::Arc; + +use super::*; + +/// A context for the document +pub type DocumentContext = Arc; + +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 { + 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, + ) { + 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); + 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> { + std::task::Poll::Ready(Err(EvalError::Unsupported)) + } + + fn poll_join( + &mut self, + _context: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Err(EvalError::Unsupported)) + } +} diff --git a/packages/document/src/elements/link.rs b/packages/document/src/elements/link.rs new file mode 100644 index 0000000000..27fd3d1ebb --- /dev/null +++ b/packages/document/src/elements/link.rs @@ -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, + pub media: Option, + pub title: Option, + pub disabled: Option, + pub r#as: Option, + pub sizes: Option, + /// Links are deduplicated by their href attribute + pub href: Option, + pub crossorigin: Option, + pub referrerpolicy: Option, + pub fetchpriority: Option, + pub hreflang: Option, + pub integrity: Option, + pub r#type: Option, + pub blocking: Option, + #[props(extends = link, extends = GlobalAttributes)] + pub additional_attributes: Vec, +} + +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", +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[doc(alias = "")] +#[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::() + .0 + .should_insert(href) +} diff --git a/packages/document/src/elements/meta.rs b/packages/document/src/elements/meta.rs new file mode 100644 index 0000000000..ea17bdb9e7 --- /dev/null +++ b/packages/document/src/elements/meta.rs @@ -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, + pub name: Option, + pub charset: Option, + pub http_equiv: Option, + pub content: Option, + #[props(extends = meta, extends = GlobalAttributes)] + pub additional_attributes: Vec, +} + +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", +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[component] +#[doc(alias = "")] +pub fn Meta(props: MetaProps) -> Element { + use_update_warning(&props, "Meta {}"); + + use_hook(|| { + let document = document(); + document.create_meta(props); + }); + + VNode::empty() +} diff --git a/packages/document/src/elements/mod.rs b/packages/document/src/elements/mod.rs new file mode 100644 index 0000000000..9f0d614225 --- /dev/null +++ b/packages/document/src/elements/mod.rs @@ -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(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> { + 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 { + match ScopeId::ROOT.has_context::() { + Some(context) => context, + None => { + let context = T::default(); + ScopeId::ROOT.provide_context(context.clone()); + context + } + } +} + +#[derive(Default, Clone)] +struct DeduplicationContext(Rc>>); + +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 + } + } +} diff --git a/packages/document/src/elements/script.rs b/packages/document/src/elements/script.rs new file mode 100644 index 0000000000..47f78fc6bd --- /dev/null +++ b/packages/document/src/elements/script.rs @@ -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, + pub defer: Option, + pub crossorigin: Option, + pub fetchpriority: Option, + pub integrity: Option, + pub nomodule: Option, + pub nonce: Option, + pub referrerpolicy: Option, + pub r#type: Option, + #[props(extends = script, extends = GlobalAttributes)] + pub additional_attributes: Vec, +} + +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> { + 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"), +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[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::() + .0 + .should_insert(src) +} diff --git a/packages/document/src/elements/style.rs b/packages/document/src/elements/style.rs new file mode 100644 index 0000000000..bb2f682153 --- /dev/null +++ b/packages/document/src/elements/style.rs @@ -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, + pub media: Option, + pub nonce: Option, + pub title: Option, + /// 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, +} + +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> { + 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; +/// }} +/// "# +/// } +/// } +/// } +/// ``` +/// +///
+/// +/// Any updates to the props after the first render will not be reflected in the head. +/// +///
+#[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::() + .0 + .should_insert(href) +} diff --git a/packages/document/src/elements/title.rs b/packages/document/src/elements/title.rs new file mode 100644 index 0000000000..2a5fe24b83 --- /dev/null +++ b/packages/document/src/elements/title.rs @@ -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 = "")] +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() +} diff --git a/packages/document/src/error.rs b/packages/document/src/error.rs new file mode 100644 index 0000000000..322b178656 --- /dev/null +++ b/packages/document/src/error.rs @@ -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 {} diff --git a/packages/document/src/eval.rs b/packages/document/src/eval.rs new file mode 100644 index 0000000000..6475c87ddc --- /dev/null +++ b/packages/document/src/eval.rs @@ -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>>; +} diff --git a/packages/html/src/js/hash.txt b/packages/document/src/js/hash.txt similarity index 100% rename from packages/html/src/js/hash.txt rename to packages/document/src/js/hash.txt diff --git a/packages/html/src/js/head.js b/packages/document/src/js/head.js similarity index 100% rename from packages/html/src/js/head.js rename to packages/document/src/js/head.js diff --git a/packages/document/src/lib.rs b/packages/document/src/lib.rs new file mode 100644 index 0000000000..05429f394a --- /dev/null +++ b/packages/document/src/lib.rs @@ -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()) +} diff --git a/packages/html/src/ts/.gitignore b/packages/document/src/ts/.gitignore similarity index 100% rename from packages/html/src/ts/.gitignore rename to packages/document/src/ts/.gitignore diff --git a/packages/html/src/ts/eval.ts b/packages/document/src/ts/eval.ts similarity index 100% rename from packages/html/src/ts/eval.ts rename to packages/document/src/ts/eval.ts diff --git a/packages/html/src/ts/head.ts b/packages/document/src/ts/head.ts similarity index 100% rename from packages/html/src/ts/head.ts rename to packages/document/src/ts/head.ts diff --git a/packages/document/tsconfig.json b/packages/document/tsconfig.json new file mode 100644 index 0000000000..11cda68e3f --- /dev/null +++ b/packages/document/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "lib": [ + "ES2015", + "DOM", + "dom", + "dom.iterable", + "ESNext" + ], + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + }, + "exclude": [ + "**/*.spec.ts" + ] +} diff --git a/packages/fullstack/src/document/server.rs b/packages/fullstack/src/document/server.rs index 74cee98c64..de602654fa 100644 --- a/packages/fullstack/src/document/server.rs +++ b/packages/fullstack/src/document/server.rs @@ -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 - } } diff --git a/packages/fullstack/src/document/web.rs b/packages/fullstack/src/document/web.rs index 66d096b01d..a1bff76017 100644 --- a/packages/fullstack/src/document/web.rs +++ b/packages/fullstack/src/document/web.rs @@ -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 - } } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index 526985108c..a265bdeb71 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -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)?; diff --git a/packages/hooks/docs/side_effects.md b/packages/hooks/docs/side_effects.md index 1f63877b8d..b1d87dbfdc 100644 --- a/packages/hooks/docs/side_effects.md +++ b/packages/hooks/docs/side_effects.md @@ -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"; diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index a123602542..362ebe4c27 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -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", ] diff --git a/packages/html/assets/script.js b/packages/html/assets/script.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/html/assets/style.css b/packages/html/assets/style.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/html/src/document/eval.rs b/packages/html/src/document/eval.rs deleted file mode 100644 index cd9d2aa0f6..0000000000 --- a/packages/html/src/document/eval.rs +++ /dev/null @@ -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 {} diff --git a/packages/html/src/document/head.rs b/packages/html/src/document/head.rs deleted file mode 100644 index c4b29c17f7..0000000000 --- a/packages/html/src/document/head.rs +++ /dev/null @@ -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 - } - } -} diff --git a/packages/html/src/document/mod.rs b/packages/html/src/document/mod.rs deleted file mode 100644 index 53eba8a040..0000000000 --- a/packages/html/src/document/mod.rs +++ /dev/null @@ -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>) -} diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 0067a90683..80e3a7c369 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -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::*; diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 8659443957..95b1b4dd6e 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -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"] } diff --git a/packages/liveview/src/eval.rs b/packages/liveview/src/eval.rs index 6821d6502f..4ba1765010 100644 --- a/packages/liveview/src/eval.rs +++ b/packages/liveview/src/eval.rs @@ -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)) } } diff --git a/packages/playwright-tests/fullstack/src/main.rs b/packages/playwright-tests/fullstack/src/main.rs index ab6a89b7b3..6dea889cc2 100644 --- a/packages/playwright-tests/fullstack/src/main.rs +++ b/packages/playwright-tests/fullstack/src/main.rs @@ -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", diff --git a/packages/playwright-tests/nested-suspense/src/main.rs b/packages/playwright-tests/nested-suspense/src/main.rs index 4d5f3e9e1b..d859948485 100644 --- a/packages/playwright-tests/nested-suspense/src/main.rs +++ b/packages/playwright-tests/nested-suspense/src/main.rs @@ -44,7 +44,7 @@ fn LoadTitle() -> Element { .unwrap(); rsx! { - Title { "{title.title}" } + document::Title { "{title.title}" } } } diff --git a/packages/playwright-tests/web/src/main.rs b/packages/playwright-tests/web/src/main.rs index e04668c133..218e2ca496 100644 --- a/packages/playwright-tests/web/src/main.rs +++ b/packages/playwright-tests/web/src/main.rs @@ -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; diff --git a/packages/router/src/history/liveview.rs b/packages/router/src/history/liveview.rs index 790b9d98e9..7504f78107 100644 --- a/packages/router/src/history/liveview.rs +++ b/packages/router/src/history/liveview.rs @@ -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({ diff --git a/packages/static-generation/src/ssg.rs b/packages/static-generation/src/ssg.rs index f43405464b..2e2d89b513 100644 --- a/packages/static-generation/src/ssg.rs +++ b/packages/static-generation/src/ssg.rs @@ -1,4 +1,5 @@ use dioxus_isrg::*; +use dioxus_lib::document::Document; use dioxus_lib::prelude::*; use dioxus_router::prelude::*; use dioxus_ssr::renderer; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index f79b2692a8..ec21e97d56 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -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 } diff --git a/packages/web/src/devtools.rs b/packages/web/src/devtools.rs index 48a6f8ea0c..438f9cab40 100644 --- a/packages/web/src/devtools.rs +++ b/packages/web/src/devtools.rs @@ -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; diff --git a/packages/web/src/document.rs b/packages/web/src/document.rs index 2b187ba624..ff9d8b4616 100644 --- a/packages/web/src/document.rs +++ b/packages/web/src/document.rs @@ -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>) } } diff --git a/packages/web/src/js/hash.txt b/packages/web/src/js/hash.txt index f397e27fa1..5002e6c9aa 100644 --- a/packages/web/src/js/hash.txt +++ b/packages/web/src/js/hash.txt @@ -1 +1 @@ -[3479327739946104450] \ No newline at end of file +[1614426347475783279] diff --git a/packages/web/src/ts/eval.ts b/packages/web/src/ts/eval.ts index 3c87d0d252..51396b63a6 100644 --- a/packages/web/src/ts/eval.ts +++ b/packages/web/src/ts/eval.ts @@ -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;