From 42c6d1772b71b061aed1987819df1eaf5e951bde Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 29 Dec 2021 13:30:48 -0500 Subject: [PATCH 1/5] publish: fix documentation page --- packages/html/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 672199a1e..b00ae38f4 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -7,7 +7,7 @@ description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Vir license = "MIT/Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" -documentation = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 639f9f53224a1a8a6699d6bbb5f46f328f645031 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 29 Dec 2021 13:31:52 -0500 Subject: [PATCH 2/5] publish: bump macro crate --- packages/core-macro/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index dee8b0a36..e3179510e 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core-macro" -version = "0.1.2" +version = "0.1.3" authors = ["Jonathan Kelley"] edition = "2021" description = "Core macro for Dioxus Virtual DOM" From 2d58d380bef83f799c81738a0a7b012e45cdb9b7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 29 Dec 2021 13:37:25 -0500 Subject: [PATCH 3/5] publish: get web ready gto publish --- packages/web/Cargo.toml | 46 +++---- packages/web/examples/async.rs | 29 ---- packages/web/examples/js_bench.rs | 221 ------------------------------ packages/web/examples/simple.rs | 44 ------ packages/web/examples/suspense.rs | 41 ------ 5 files changed, 17 insertions(+), 364 deletions(-) delete mode 100644 packages/web/examples/async.rs delete mode 100644 packages/web/examples/js_bench.rs delete mode 100644 packages/web/examples/simple.rs delete mode 100644 packages/web/examples/suspense.rs diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index cb540dae5..87521e35e 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-web" -version = "0.0.0" +version = "0.0.1" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" @@ -9,20 +9,12 @@ repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.3"} -dioxus-html = { path = "../html" } +dioxus-core = { path = "../core", version = "^0.1.4" } +dioxus-html = { path = "../html", version = "^0.1.1" } js-sys = "0.3" -# wasm-bindgen-shared = { path = "../../../Tinkering/wasm-bindgen/crates/shared" } -# wasm-bindgen-macro-support = { path = "../../../Tinkering/wasm-bindgen/crates/macro-support" } -# wasm-bindgen = { features = [ - - wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } -# wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } -# wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.20" log = { version = "0.4.14", features = ["release_max_level_off"] } @@ -78,23 +70,19 @@ features = [ ] -[lib] -crate-type = ["cdylib", "rlib"] +# [lib] +# crate-type = ["cdylib", "rlib"] -[dev-dependencies] -im-rc = "15.0.0" -separator = "0.4.1" -uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } -serde = { version = "1.0.126", features = ["derive"] } -reqwest = { version = "0.11", features = ["json"] } -dioxus-hooks = { path = "../hooks" } -dioxus-core-macro = { path = "../core-macro" } -rand = { version = "0.8.4", features = ["small_rng"] } +# [dev-dependencies] +# im-rc = "15.0.0" +# separator = "0.4.1" +# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } +# serde = { version = "1.0.126", features = ["derive"] } +# reqwest = { version = "0.11", features = ["json"] } +# dioxus-hooks = { path = "../hooks" } +# dioxus-core-macro = { path = "../core-macro" } +# rand = { version = "0.8.4", features = ["small_rng"] } -[dev-dependencies.getrandom] -version = "0.2" -features = ["js"] - -# surf = { version = "2.3.1", default-features = false, features = [ -# "wasm-client", -# ] } +# [dev-dependencies.getrandom] +# version = "0.2" +# features = ["js"] diff --git a/packages/web/examples/async.rs b/packages/web/examples/async.rs deleted file mode 100644 index c045ca5e4..000000000 --- a/packages/web/examples/async.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Example: README.md showcase -//! -//! The example from the README.md. - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_hooks::use_state; -use dioxus_html as dioxus_elements; -use dioxus_web; -use gloo_timers::future::TimeoutFuture; - -fn main() { - dioxus_web::launch(App); -} - -static App: Component = |cx| { - let mut count = use_state(&cx, || 0); - - cx.push_future(|| async move { - TimeoutFuture::new(100).await; - count += 1; - }); - - rsx!(cx, div { - h3 { "High-Five counter: {count}" } - button { onclick: move |_| count.set(0), "Reset!" } - }) -}; diff --git a/packages/web/examples/js_bench.rs b/packages/web/examples/js_bench.rs deleted file mode 100644 index d9ddc7e1c..000000000 --- a/packages/web/examples/js_bench.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::cell::Cell; - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_hooks::{use_ref, use_state}; -use dioxus_html as dioxus_elements; -use dioxus_web; -use rand::prelude::*; - -fn main() { - console_error_panic_hook::set_once(); - if cfg!(debug_assertions) { - wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); - log::debug!("hello world"); - } - - for a in ADJECTIVES { - wasm_bindgen::intern(*a); - } - for a in COLOURS { - wasm_bindgen::intern(*a); - } - for a in NOUNS { - wasm_bindgen::intern(*a); - } - for a in [ - "container", - "jumbotron", - "row", - "Dioxus", - "col-md-6", - "col-md-1", - "Create 1,000 rows", - "run", - "Create 10,000 rows", - "runlots", - "Append 1,000 rows", - "add", - "Update every 10th row", - "update", - "Clear", - "clear", - "Swap rows", - "swaprows", - "preloadicon glyphicon glyphicon-remove", // - "aria-hidden", - "onclick", - "true", - "false", - "danger", - "type", - "id", - "class", - "glyphicon glyphicon-remove remove", - "dioxus-id", - "dioxus-event-click", - "dioxus", - "click", - "1.10", - "lbl", - "remove", - "dioxus-event", - "col-sm-6 smallpad", - "btn btn-primary btn-block", - "", - " ", - ] { - wasm_bindgen::intern(a); - } - for x in 0..100_000 { - wasm_bindgen::intern(&x.to_string()); - } - - dioxus_web::launch(App); -} - -#[derive(Clone, PartialEq, Copy)] -struct Label { - key: usize, - labels: [&'static str; 3], -} - -static mut Counter: Cell = Cell::new(1); - -impl Label { - fn new_list(num: usize) -> Vec { - let mut rng = SmallRng::from_entropy(); - let mut labels = Vec::with_capacity(num); - - let offset = unsafe { Counter.get() }; - unsafe { Counter.set(offset + num) }; - - for k in offset..(offset + num) { - labels.push(Label { - key: k, - labels: [ - ADJECTIVES.choose(&mut rng).unwrap(), - COLOURS.choose(&mut rng).unwrap(), - NOUNS.choose(&mut rng).unwrap(), - ], - }); - } - - labels - } -} - -static App: Component = |cx| { - let mut items = use_ref(&cx, || vec![]); - let mut selected = use_state(&cx, || None); - - cx.render(rsx! { - div { class: "container" - div { class: "jumbotron" - div { class: "row" - div { class: "col-md-6", h1 { "Dioxus" } } - div { class: "col-md-6" - div { class: "row" - ActionButton { name: "Create 1,000 rows", id: "run", - onclick: move || items.set(Label::new_list(1_000)), - } - ActionButton { name: "Create 10,000 rows", id: "runlots", - onclick: move || items.set(Label::new_list(10_000)), - } - ActionButton { name: "Append 1,000 rows", id: "add", - onclick: move || items.write().extend(Label::new_list(1_000)), - } - ActionButton { name: "Update every 10th row", id: "update", - onclick: move || items.write().iter_mut().step_by(10).for_each(|item| item.labels[2] = "!!!"), - } - ActionButton { name: "Clear", id: "clear", - onclick: move || items.write().clear(), - } - ActionButton { name: "Swap Rows", id: "swaprows", - onclick: move || items.write().swap(0, 998), - } - } - } - } - } - table { class: "table table-hover table-striped test-data" - tbody { id: "tbody" - {items.read().iter().enumerate().map(|(id, item)| { - let [adj, col, noun] = item.labels; - let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""}; - rsx!(tr { - class: "{is_in_danger}", - key: "{id}", - td { class:"col-md-1" } - td { class:"col-md-1", "{item.key}" } - td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), - a { class: "lbl", "{adj} {col} {noun}" } - } - td { class: "col-md-1" - a { class: "remove", onclick: move |_| { items.write().remove(id); }, - span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } - } - } - td { class: "col-md-6" } - }) - })} - } - } - span { class: "preloadicon glyphicon glyphicon-remove" aria_hidden: "true" } - } - }) -}; - -#[derive(Props)] -struct ActionButtonProps<'a> { - name: &'static str, - id: &'static str, - onclick: &'a dyn Fn(), -} - -fn ActionButton<'a>(cx: Scope<'a, ActionButtonProps<'a>>) -> Element { - rsx!(cx, div { class: "col-sm-6 smallpad" - button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.props.id}", onclick: move |_| (cx.props.onclick)(), - "{cx.props.name}" - } - }) -} - -static ADJECTIVES: &[&str] = &[ - "pretty", - "large", - "big", - "small", - "tall", - "short", - "long", - "handsome", - "plain", - "quaint", - "clean", - "elegant", - "easy", - "angry", - "crazy", - "helpful", - "mushy", - "odd", - "unsightly", - "adorable", - "important", - "inexpensive", - "cheap", - "expensive", - "fancy", -]; - -static COLOURS: &[&str] = &[ - "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", - "orange", -]; - -static NOUNS: &[&str] = &[ - "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", - "pizza", "mouse", "keyboard", -]; diff --git a/packages/web/examples/simple.rs b/packages/web/examples/simple.rs deleted file mode 100644 index d3f2f078b..000000000 --- a/packages/web/examples/simple.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Example: README.md showcase -//! -//! The example from the README.md. - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_hooks::use_state; -use dioxus_html as dioxus_elements; - -fn main() { - wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); - dioxus_web::launch(App); -} - -static App: Component = |cx| { - let show = use_state(&cx, || true); - - let inner = match *show { - true => { - rsx!( div { - "hello world" - }) - } - false => { - rsx!( div { - // h1 { - "bello world" - // } - }) - } - }; - - rsx!(cx, div { - button { - "toggle" - onclick: move |_| { - let cur = *show; - show.set(!cur); - } - } - {inner} - }) -}; diff --git a/packages/web/examples/suspense.rs b/packages/web/examples/suspense.rs deleted file mode 100644 index 30a15a123..000000000 --- a/packages/web/examples/suspense.rs +++ /dev/null @@ -1,41 +0,0 @@ -#![allow(non_upper_case_globals)] - -//! Example: README.md showcase -//! -//! The example from the README.md. - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; -use dioxus_web; - -fn main() { - dioxus_web::launch(App); -} - -static App: Component = |cx| { - todo!("suspense is broken") - // let doggo = suspend(|| async move { - // #[derive(serde::Deserialize)] - // struct Doggo { - // message: String, - // } - - // let src = reqwest::get("https://dog.ceo/api/breeds/image/random") - // .await - // .expect("Failed to fetch doggo") - // .json::() - // .await - // .expect("Failed to parse doggo") - // .message; - - // rsx!(cx, img { src: "{src}" }) - // }); - - // rsx!(cx, div { - // h1 {"One doggo coming right up"} - // button { onclick: move |_| cx.needs_update(), "Get a new doggo" } - // {doggo} - // }) -}; From 14961023f927b3a8bde83cfc7883aa8bfcca9e85 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 29 Dec 2021 21:28:28 -0500 Subject: [PATCH 4/5] wip: polish some more things --- Cargo.toml | 22 ++- examples/README.md | 16 +- examples/async.rs | 12 +- examples/borrowed.rs | 2 +- examples/calculator.rs | 194 ++++++++++--------- examples/core/jsframework.rs | 2 +- examples/desktop/todomvc.rs | 5 - examples/file_explorer.rs | 1 - examples/hello_world.rs | 2 +- examples/readme.rs | 7 +- examples/tasks.rs | 18 +- examples/webview_web.rs | 13 +- packages/core-macro/src/lib.rs | 2 +- packages/core/Cargo.toml | 1 - packages/core/README.md | 2 +- packages/core/examples/component_children.rs | 2 +- packages/core/examples/works.rs | 2 +- packages/core/src/nodes.rs | 12 +- packages/core/src/scopes.rs | 2 +- packages/core/src/virtual_dom.rs | 18 +- packages/core/tests/borrowedstate.rs | 2 +- packages/core/tests/miri_stress.rs | 14 +- packages/desktop/Cargo.toml | 1 - packages/desktop/examples/async.rs | 24 +-- packages/hooks/README.md | 47 +---- packages/hooks/src/lib.rs | 46 ++++- packages/hooks/src/usestate.rs | 134 ++----------- packages/html/Cargo.toml | 2 - packages/liveview/README.md | 2 +- packages/mobile/Cargo.toml | 5 +- packages/mobile/src/lib.rs | 5 - src/lib.rs | 18 +- 32 files changed, 265 insertions(+), 370 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44750d463..01d8c0370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,19 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "./packages/core", version = "^0.1.3" } -dioxus-html = { path = "./packages/html", optional = true } -dioxus-core-macro = { path = "./packages/core-macro", optional = true } +dioxus-core = { path = "./packages/core", version = "^0.1.4" } +dioxus-html = { path = "./packages/html", version = "^0.1.1", optional = true } +dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.3", optional = true } dioxus-hooks = { path = "./packages/hooks", optional = true } -dioxus-ssr = { path = "./packages/ssr", optional = true } -dioxus-web = { path = "./packages/web", optional = true } +dioxus-web = { path = "./packages/web", version = "^0.0.1", optional = true } dioxus-desktop = { path = "./packages/desktop", optional = true } +dioxus-ssr = { path = "./packages/ssr", optional = true } + dioxus-router = { path = "./packages/router", optional = true } -dioxus-mobile = { path = "./packages/mobile", optional = true } -dioxus-liveview = { path = "./packages/liveview", optional = true } +# dioxus-mobile = { path = "./packages/mobile", optional = true } +# dioxus-liveview = { path = "./packages/liveview", optional = true } [features] default = ["macro", "hooks", "html"] @@ -31,11 +32,13 @@ macro = ["dioxus-core-macro"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] router = ["dioxus-router"] -liveview = ["dioxus-liveview"] ssr = ["dioxus-ssr"] web = ["dioxus-web", "dioxus-router/web"] desktop = ["dioxus-desktop", "dioxus-router/desktop"] -mobile = ["dioxus-mobile"] + + +# mobile = ["dioxus-mobile"] +# liveview = ["dioxus-liveview"] [workspace] @@ -60,7 +63,6 @@ im-rc = "15.0.0" fxhash = "0.2.1" anyhow = "1.0.51" serde_json = "1.0.73" -simple_logger = "1.16.0" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] argh = "0.1.7" diff --git a/examples/README.md b/examples/README.md index eabb0f57d..eb0037683 100644 --- a/examples/README.md +++ b/examples/README.md @@ -64,7 +64,7 @@ Here's what a few common tasks look like in Dioxus: Nested components with children and internal state: ```rust -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { cx.render(rsx!( Toggle { "Toggle me" } )) } @@ -86,7 +86,7 @@ fn Toggle(cx: Scope) -> Element { Controlled inputs: ```rust -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let value = use_state(&cx, String::new); cx.render(rsx!( input { @@ -100,7 +100,7 @@ fn App(cx: Scope<()>) -> Element { Lists and Conditional rendering: ```rust -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let list = (0..10).map(|i| { rsx!(li { key: "{i}", "Value: {i}" }) }); @@ -128,7 +128,7 @@ static App: Component = |cx, _| rsx!(cx, div {"hello world!"}); Borrowed prop contents: ```rust -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let name = use_state(&cx, || String::from("example")); rsx!(cx, Child { title: name.as_str() }) } @@ -145,12 +145,12 @@ Global State ```rust struct GlobalState { name: String } -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { use_provide_shared_state(cx, || GlobalState { name: String::from("Toby") }) rsx!(cx, Leaf {}) } -fn Leaf(cx: Scope<()>) -> Element { +fn Leaf(cx: Scope) -> Element { let state = use_consume_shared_state::(cx)?; rsx!(cx, "Hello {state.name}") } @@ -166,7 +166,7 @@ enum Route { Post(id) } -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let route = use_router(cx, Route::parse); cx.render(rsx!(div { {match route { @@ -179,7 +179,7 @@ fn App(cx: Scope<()>) -> Element { Suspense ```rust -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let doggo = use_suspense(cx, || async { reqwest::get("https://dog.ceo/api/breeds/image/random").await.unwrap().json::().await.unwrap() }, |response| cx.render(rsx!( img { src: "{response.message}" })) diff --git a/examples/async.rs b/examples/async.rs index c473ead72..e399ea016 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -8,19 +8,19 @@ use gloo_timers::future::TimeoutFuture; #[tokio::main] async fn main() { - dioxus::desktop::launch(App); + dioxus::desktop::launch(app); } -pub static App: Component = |cx| { +fn app(cx: Scope) -> Element { let count = use_state(&cx, || 0); - let mut direction = use_state(&cx, || 1); + let direction = use_state(&cx, || 1); let (async_count, dir) = (count.for_async(), *direction); let task = use_coroutine(&cx, move || async move { loop { TimeoutFuture::new(250).await; - *async_count.get_mut() += dir; + *async_count.modify() += dir; } }); @@ -37,9 +37,9 @@ pub static App: Component = |cx| { button { "Switch counting direcion" onclick: move |_| { - direction *= -1; + *direction.modify() *= -1; task.restart(); } } }) -}; +} diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 3b05b6f9f..29c502e13 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -20,7 +20,7 @@ fn main() { dioxus::desktop::launch(App); } -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let text: &mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f); let first = text.get_mut(0).unwrap(); diff --git a/examples/calculator.rs b/examples/calculator.rs index c7490a1b4..c3814c088 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + /* This example is a simple iOS-style calculator. This particular example can run any platform - Web, Mobile, Desktop. This calculator version uses React-style state management. All state is held as individual use_states. @@ -10,10 +12,10 @@ use dioxus::prelude::*; use separator::Separatable; fn main() { - dioxus::desktop::launch(APP); + dioxus::desktop::launch(app); } -static APP: Component = |cx| { +fn app(cx: Scope) -> Element { let cur_val = use_state(&cx, || 0.0_f64); let operator = use_state(&cx, || None as Option<&'static str>); let display_value = use_state(&cx, || String::from("")); @@ -21,109 +23,111 @@ static APP: Component = |cx| { let toggle_percent = move |_| todo!(); let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str()); - rsx!(cx, div { - class: "calculator", - onkeydown: move |evt| match evt.key_code { - KeyCode::Add => operator.set(Some("+")), - KeyCode::Subtract => operator.set(Some("-")), - KeyCode::Divide => operator.set(Some("/")), - KeyCode::Multiply => operator.set(Some("*")), - KeyCode::Num0 => input_digit(0), - KeyCode::Num1 => input_digit(1), - KeyCode::Num2 => input_digit(2), - KeyCode::Num3 => input_digit(3), - KeyCode::Num4 => input_digit(4), - KeyCode::Num5 => input_digit(5), - KeyCode::Num6 => input_digit(6), - KeyCode::Num7 => input_digit(7), - KeyCode::Num8 => input_digit(8), - KeyCode::Num9 => input_digit(9), - KeyCode::Backspace => { - if !display_value.as_str().eq("0") { - display_value.modify().pop(); - } - } - _ => {} - } - div { class: "calculator-display", {[format_args!("{}", cur_val.separated_string())]} } - div { class: "input-keys" - div { class: "function-keys" - CalculatorKey { - {[if display_value == "0" { "C" } else { "AC" }]} - name: "key-clear", - onclick: move |_| { - display_value.set("0".to_string()); - if display_value != "0" { - operator.set(None); - cur_val.set(0.0); - } + cx.render(rsx!( + div { class: "calculator", + onkeydown: move |evt| match evt.key_code { + KeyCode::Add => operator.set(Some("+")), + KeyCode::Subtract => operator.set(Some("-")), + KeyCode::Divide => operator.set(Some("/")), + KeyCode::Multiply => operator.set(Some("*")), + KeyCode::Num0 => input_digit(0), + KeyCode::Num1 => input_digit(1), + KeyCode::Num2 => input_digit(2), + KeyCode::Num3 => input_digit(3), + KeyCode::Num4 => input_digit(4), + KeyCode::Num5 => input_digit(5), + KeyCode::Num6 => input_digit(6), + KeyCode::Num7 => input_digit(7), + KeyCode::Num8 => input_digit(8), + KeyCode::Num9 => input_digit(9), + KeyCode::Backspace => { + if !display_value.as_str().eq("0") { + display_value.modify().pop(); } } - CalculatorKey { - "±" - name: "key-sign", - onclick: move |_| { - if display_value.starts_with("-") { - display_value.set(display_value.trim_start_matches("-").to_string()) - } else { - display_value.set(format!("-{}", *display_value)) - } - }, - } - CalculatorKey { - "%" - onclick: {toggle_percent} - name: "key-percent", - } + _ => {} } - div { class: "digit-keys" - CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "●" } + div { class: "calculator-display", {[cur_val.separated_string()]} } + div { class: "input-keys" + div { class: "function-keys" + CalculatorKey { + {[if display_value == "0" { "C" } else { "AC" }]} + name: "key-clear", + onclick: move |_| { + display_value.set("0".to_string()); + if display_value != "0" { + operator.set(None); + cur_val.set(0.0); + } + } + } + CalculatorKey { + "±" + name: "key-sign", + onclick: move |_| { + if display_value.starts_with("-") { + display_value.set(display_value.trim_start_matches("-").to_string()) + } else { + display_value.set(format!("-{}", *display_value)) + } + }, + } + CalculatorKey { + "%" + onclick: {toggle_percent} + name: "key-percent", + } + } + div { class: "digit-keys" + CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } + CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "●" } - {(1..9).map(|k| rsx!{ - CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } - })} - } - div { class: "operator-keys" - CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some("/")) "÷" } - CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "×" } - CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "−" } - CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" } - CalculatorKey { - "=" - name: "key-equals", - onclick: move |_| { - if let Some(op) = operator.as_ref() { - let rhs = display_value.parse::().unwrap(); - let new_val = match *op { - "+" => *cur_val + rhs, - "-" => *cur_val - rhs, - "*" => *cur_val * rhs, - "/" => *cur_val / rhs, - _ => unreachable!(), - }; - cur_val.set(new_val); - display_value.set(new_val.to_string()); - operator.set(None); - } - }, + {(1..9).map(|k| rsx!{ + CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } + })} + } + div { class: "operator-keys" + CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some("/")) "÷" } + CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "×" } + CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "−" } + CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" } + CalculatorKey { + "=" + name: "key-equals", + onclick: move |_| { + if let Some(op) = operator.as_ref() { + let rhs = display_value.parse::().unwrap(); + let new_val = match *op { + "+" => *cur_val + rhs, + "-" => *cur_val - rhs, + "*" => *cur_val * rhs, + "/" => *cur_val / rhs, + _ => unreachable!(), + }; + cur_val.set(new_val); + display_value.set(new_val.to_string()); + operator.set(None); + } + }, + } } } } - }) -}; + )) +} -#[derive(Props)] -struct CalculatorKeyProps<'a> { +#[inline_props] +fn CalculatorKey<'a>( + cx: Scope, name: &'static str, onclick: &'a dyn Fn(Arc), children: Element<'a>, -} - -fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element { - rsx!(cx, button { - class: "calculator-key {cx.props.name}" - onclick: {cx.props.onclick} - {&cx.props.children} +) -> Element { + cx.render(rsx! { + button { + class: "calculator-key {name}" + onclick: {onclick} + {children} + } }) } diff --git a/examples/core/jsframework.rs b/examples/core/jsframework.rs index 15c25ec8d..f9fa9c431 100644 --- a/examples/core/jsframework.rs +++ b/examples/core/jsframework.rs @@ -15,7 +15,7 @@ fn main() { assert!(g.edits.len() > 1); } -fn App((cx, props): Scope<()>) -> Element { +fn App((cx, props): Scope) -> Element { let mut rng = SmallRng::from_entropy(); let rows = (0..10_000_usize).map(|f| { let label = Label::new(&mut rng); diff --git a/examples/desktop/todomvc.rs b/examples/desktop/todomvc.rs index ff982a585..e70ed1806 100644 --- a/examples/desktop/todomvc.rs +++ b/examples/desktop/todomvc.rs @@ -5,15 +5,10 @@ use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_hooks::*; use dioxus_html as dioxus_elements; -use simple_logger::SimpleLogger; use std::collections::HashMap; fn main() { - if cfg!(debug_assertions) { - SimpleLogger::new().init().unwrap(); - } - dioxus_desktop::launch(App) } diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index a2f168d6b..05f5c4ca7 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -8,7 +8,6 @@ use dioxus::prelude::*; fn main() { - // simple_logger::init_with_level(log::Level::Debug); dioxus::desktop::launch_cfg(App, |c| { c.with_window(|w| { w.with_resizable(true).with_inner_size( diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 24f54883a..ab7bfffa3 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -4,7 +4,7 @@ fn main() { dioxus::desktop::launch(app); } -fn app(cx: Scope<()>) -> Element { +fn app(cx: Scope) -> Element { cx.render(rsx! ( div { "Hello, world!" } )) diff --git a/examples/readme.rs b/examples/readme.rs index c3f0a3de7..9f0a288e3 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -3,18 +3,19 @@ //! The example from the README.md. use dioxus::prelude::*; + fn main() { dioxus::desktop::launch(App); } static App: Component = |cx| { - let mut count = use_state(&cx, || 0); + let count = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| *count.modify() += 1, "Up high!" } + button { onclick: move |_| *count.modify() -= 1, "Down low!" } } }) }; diff --git a/examples/tasks.rs b/examples/tasks.rs index 0db459786..a378c24a6 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -2,26 +2,30 @@ //! //! The example from the README.md. +use dioxus::prelude::*; use std::time::Duration; -use dioxus::prelude::*; fn main() { dioxus::desktop::launch(app); } -fn app(cx: Scope<()>) -> Element { - let mut count = use_state(&cx, || 0); +fn app(cx: Scope) -> Element { + let count = use_state(&cx, || 0); - cx.push_future(|| async move { - tokio::time::sleep(Duration::from_millis(100)).await; - count += 1; + use_future(&cx, || { + for_async![count]; + async move { + while let _ = tokio::time::sleep(Duration::from_millis(1000)).await { + *count.modify() += 1; + } + } }); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } button { - onclick: move |_| count +=1 , + onclick: move |_| *count.modify() += 1, "Click me!" } } diff --git a/examples/webview_web.rs b/examples/webview_web.rs index 367094b2c..5a7df845d 100644 --- a/examples/webview_web.rs +++ b/examples/webview_web.rs @@ -1,4 +1,3 @@ -#![allow(non_upper_case_globals, non_snake_case)] //! Example: Webview Renderer //! ------------------------- //! @@ -13,17 +12,17 @@ use dioxus::prelude::*; fn main() { - dioxus::web::launch(App); + dioxus::desktop::launch(app); } -static App: Component = |cx| { - let mut count = use_state(&cx, || 0); +fn app(cx: Scope) -> Element { + let count = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "Hifive counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| *count.modify() += 1, "Up high!" } + button { onclick: move |_| *count.modify() -= 1, "Down low!" } } }) -}; +} diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index a196441b8..2d17f4bbc 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -161,7 +161,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// pub struct BallerProps {} /// /// /// This component totally balls -/// pub fn Baller(cx: Scope<()>) -> DomTree { +/// pub fn Baller(cx: Scope) -> DomTree { /// todo!() /// } /// } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index cd96fca04..53d169760 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -50,7 +50,6 @@ anyhow = "1.0.42" dioxus-html = { path = "../html" } fern = { version = "0.6.0", features = ["colored"] } rand = { version = "0.8.4", features = ["small_rng"] } -simple_logger = "1.13.0" dioxus-core-macro = { path = "../core-macro", version = "^0.1.2" } criterion = "0.3.5" thiserror = "1.0.30" diff --git a/packages/core/README.md b/packages/core/README.md index 672a1dc78..87d724415 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -6,7 +6,7 @@ To build new apps with Dioxus or to extend the ecosystem with new hooks or compo ```rust -fn app(cx: Scope<()>) -> Element { +fn app(cx: Scope) -> Element { rsx!(cx, div { "hello world" }) } diff --git a/packages/core/examples/component_children.rs b/packages/core/examples/component_children.rs index b6bbf8a86..73d0750a6 100644 --- a/packages/core/examples/component_children.rs +++ b/packages/core/examples/component_children.rs @@ -11,7 +11,7 @@ fn main() { dbg!(edits); } -fn parent(cx: Scope<()>) -> Element { +fn parent(cx: Scope) -> Element { let value = cx.use_hook(|_| String::new(), |f| f); cx.render(rsx! { diff --git a/packages/core/examples/works.rs b/packages/core/examples/works.rs index f521dcc42..d108b6d07 100644 --- a/packages/core/examples/works.rs +++ b/packages/core/examples/works.rs @@ -9,7 +9,7 @@ fn main() { let _ = VirtualDom::new(parent); } -fn parent(cx: Scope<()>) -> Element { +fn parent(cx: Scope) -> Element { let value = cx.use_hook(|_| String::new(), |f| f); cx.render(rsx! { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index fc0e38f59..2ebd1e250 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -84,7 +84,7 @@ pub enum VNode<'src> { /// # Example /// /// ```rust, ignore - /// fn Example(cx: Scope<()>) -> Element { + /// fn Example(cx: Scope) -> Element { /// ... /// } /// @@ -740,6 +740,12 @@ impl IntoVNode<'_> for &'static str { } } +impl IntoVNode<'_> for String { + fn into_vnode(self, cx: NodeFactory) -> VNode { + cx.text(format_args!("{}", self)) + } +} + impl IntoVNode<'_> for Arguments<'_> { fn into_vnode(self, cx: NodeFactory) -> VNode { cx.text(self) @@ -758,3 +764,7 @@ impl<'a> IntoVNode<'a> for &VNode<'a> { self.decouple() } } + +trait IntoAcceptedVnode<'a> { + fn into_accepted_vnode(self, cx: NodeFactory<'a>) -> VNode<'a>; +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 42e16d223..5d35b298d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -490,7 +490,7 @@ impl ScopeState { /// # Example /// /// ```rust, ignore - /// fn App(cx: Scope<()>) -> Element { + /// fn App(cx: Scope) -> Element { /// rsx!(cx, div { "Subtree {id}"}) /// }; /// ``` diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 09df37bb8..e96527e95 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -80,7 +80,7 @@ use std::{any::Any, collections::VecDeque, iter::FromIterator, pin::Pin, sync::A /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. /// /// ```rust, ignore -/// fn App(cx: Scope<()>) -> Element { +/// fn App(cx: Scope) -> Element { /// cx.render(rsx!{ /// div { "Hello World" } /// }) @@ -127,7 +127,7 @@ impl VirtualDom { /// /// # Example /// ```rust, ignore - /// fn Example(cx: Scope<()>) -> Element { + /// fn Example(cx: Scope) -> Element { /// cx.render(rsx!( div { "hello world" } )) /// } /// @@ -388,7 +388,7 @@ impl VirtualDom { /// # Example /// /// ```rust, ignore - /// fn App(cx: Scope<()>) -> Element { + /// fn App(cx: Scope) -> Element { /// cx.render(rsx!( div {"hello"} )) /// } /// @@ -544,7 +544,7 @@ impl VirtualDom { /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. /// /// ```rust - /// fn Base(cx: Scope<()>) -> Element { + /// fn Base(cx: Scope) -> Element { /// rsx!(cx, div {}) /// } /// @@ -564,7 +564,7 @@ impl VirtualDom { /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. /// /// ```rust - /// fn Base(cx: Scope<()>) -> Element { + /// fn Base(cx: Scope) -> Element { /// rsx!(cx, div {}) /// } /// @@ -586,7 +586,7 @@ impl VirtualDom { /// /// /// ```rust - /// fn Base(cx: Scope<()>) -> Element { + /// fn Base(cx: Scope) -> Element { /// rsx!(cx, div {}) /// } /// @@ -609,7 +609,7 @@ impl VirtualDom { /// /// /// ```rust - /// fn Base(cx: Scope<()>) -> Element { + /// fn Base(cx: Scope) -> Element { /// rsx!(cx, div {}) /// } /// @@ -753,7 +753,7 @@ pub enum SchedulerMsg { /// /// # Example /// ```rust -/// fn App(cx: Scope<()>) -> Element { +/// fn App(cx: Scope) -> Element { /// rsx!(cx, div { /// onclick: move |_| println!("Clicked!") /// }) @@ -871,7 +871,7 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> { /// ## Example /// /// ```rust, ignore -/// fn App(cx: Scope<()>) -> Element { +/// fn App(cx: Scope) -> Element { /// cx.render(rsx!{ /// CustomCard { /// h1 {}2 diff --git a/packages/core/tests/borrowedstate.rs b/packages/core/tests/borrowedstate.rs index f58e97635..c143c2fe1 100644 --- a/packages/core/tests/borrowedstate.rs +++ b/packages/core/tests/borrowedstate.rs @@ -10,7 +10,7 @@ fn test_borrowed_state() { let _ = VirtualDom::new(Parent); } -fn Parent(cx: Scope<()>) -> Element { +fn Parent(cx: Scope) -> Element { let value = cx.use_hook(|_| String::new(), |f| &*f); cx.render(rsx! { diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 2091a8e3a..ebf38c7c1 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -28,7 +28,7 @@ fn new_dom(app: Component

, props: P) -> VirtualDom { /// In debug, this should also toss a warning. #[test] fn test_memory_leak() { - fn app(cx: Scope<()>) -> Element { + fn app(cx: Scope) -> Element { let val = cx.use_hook(|_| 0, |f| f); *val += 1; @@ -68,7 +68,7 @@ fn test_memory_leak() { }) } - fn child(cx: Scope<()>) -> Element { + fn child(cx: Scope) -> Element { rsx!(cx, div { "goodbye world" }) } @@ -85,7 +85,7 @@ fn test_memory_leak() { #[test] fn memo_works_properly() { - fn app(cx: Scope<()>) -> Element { + fn app(cx: Scope) -> Element { let val = cx.use_hook(|_| 0, |f| f); *val += 1; @@ -164,7 +164,7 @@ fn free_works_on_root_props() { #[test] fn free_works_on_borrowed() { - fn app(cx: Scope<()>) -> Element { + fn app(cx: Scope) -> Element { cx.render(rsx! { child(a: "alpha", b: "asd".to_string()) }) @@ -203,7 +203,7 @@ fn free_works_on_root_hooks() { } } - fn app(cx: Scope<()>) -> Element { + fn app(cx: Scope) -> Element { let name = cx.use_hook(|_| Droppable(String::from("asd")), |f| f); rsx!(cx, div { "{name.0}" }) } @@ -214,7 +214,7 @@ fn free_works_on_root_hooks() { #[test] fn old_props_arent_stale() { - fn app(cx: Scope<()>) -> Element { + fn app(cx: Scope) -> Element { dbg!("rendering parent"); let cnt = cx.use_hook(|_| 0, |f| f); *cnt += 1; @@ -261,7 +261,7 @@ fn old_props_arent_stale() { #[test] fn basic() { - fn app(cx: Scope<()>) -> Element { + fn app(cx: Scope) -> Element { rsx!(cx, div { child(a: "abcdef".to_string()) }) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index a132bc513..d088aa04c 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -37,4 +37,3 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } -simple_logger = "1.13.0" diff --git a/packages/desktop/examples/async.rs b/packages/desktop/examples/async.rs index 2c6901f85..056e7b591 100644 --- a/packages/desktop/examples/async.rs +++ b/packages/desktop/examples/async.rs @@ -9,22 +9,18 @@ fn main() { dioxus_desktop::launch(app); } -fn app(cx: Scope<()>) -> Element { +fn app(cx: Scope) -> Element { let count = use_state(&cx, || 0); - // push the futureo on initialization - cx.use_hook( - |_| { - cx.push_future({ - let count = count.for_async(); - async move { - tokio::time::sleep(Duration::from_millis(1000)).await; - *count.get_mut() += 1; - } - }); - }, - |_| {}, - ); + use_future(&cx, || { + let count = count.for_async(); + async move { + loop { + tokio::time::sleep(Duration::from_millis(1000)).await; + *count.modify() += 1; + } + } + }); cx.render(rsx! { div { diff --git a/packages/hooks/README.md b/packages/hooks/README.md index 11e008c44..18133e40b 100644 --- a/packages/hooks/README.md +++ b/packages/hooks/README.md @@ -4,58 +4,19 @@ This crate includes some basic useful hooks for dioxus: - use_state - use_ref -- use_collection -- use_task -- use_signal +- use_future +- use_coroutine ## use_state -The king daddy of state hooks. +The primary mechanism of stored state. You can always use it "normally" with the `split` method: ```rust -// Normal usage: +// Rusty-smart-pointer usage: let value = use_state(&cx, || 10); // "Classic" usage: let (value, set_value) = use_state(&cx, || 0).split(); ``` - -## use_ref - - -## use_rwlock -A multithreaded form of RwLock for use in tasks -```rust -let val = use_rwlock(cx, || 10); -use_task((), || async loop { - *val.write().unwrap() += 1; - async_std::task::delay(Duration::from_ms(1000)).await; -}); -use_task((), || async loop { - *val.write().unwrap() -= 1; - async_std::task::delay(Duration::from_ms(500)).await; -}); -``` - -## use_hashmap -Store a memoized collection with similar semantics to use_state. Comes with a bunch of utility methods to make working with collections easier. Is essentially a wrapper over the immutable hashmap in im-rc. - -```rust -let todos = use_hashmap(cx, |map| map.insert("bob", "bill")); -cx.render(rsx!( - button { onclick: move |_| todos.insert("bob", "bill") - "add random todo" - } -) - -``` - -## use_task - -use_task submits a task to the dioxus task queue to be progressed during Dioxus's async event loop. The task must not return anything - - -## use_signal - diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 1a38ed172..0050d1779 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,5 +1,5 @@ mod usestate; -pub use usestate::{use_state, AsyncUseState, UseState}; +pub use usestate::{use_state, UseState}; mod useref; pub use useref::*; @@ -16,5 +16,45 @@ pub use usefuture::*; mod usesuspense; pub use usesuspense::*; -// mod usemodel; -// pub use usemodel::*; +#[macro_export] +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} + +/// Calls `for_async` on the series of paramters. +/// +/// If the type is Clone, then it will be cloned. However, if the type is not `clone` +/// then it must have a `for_async` method for Rust to lower down into. +/// +/// See: how use_state implements `for_async` but *not* through the trait. +#[macro_export] +macro_rules! for_async { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.for_async(); + )*} +} + +/// This is a marker trait that uses decoherence. +/// +/// It is *not* meant for hooks to actually implement, but rather defer to their +/// underlying implementation if they *don't* implement the trait. +/// +/// +pub trait AsyncHook { + type Output; + fn for_async(self) -> Self::Output; +} + +impl AsyncHook for T +where + T: ToOwned, +{ + type Output = T; + fn for_async(self) -> Self::Output { + self + } +} diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 88caea82d..6ef67efcb 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -2,20 +2,9 @@ use dioxus_core::prelude::*; use std::{ cell::{Cell, Ref, RefCell, RefMut}, fmt::{Debug, Display}, - ops::Not, rc::Rc, }; -pub trait UseStateA<'a, T> { - fn use_state(&self, initial_state_fn: impl FnOnce() -> T) -> UseState<'a, T>; -} - -impl<'a, P, T> UseStateA<'a, T> for Scope<'a, P> { - fn use_state(&self, initial_state_fn: impl FnOnce() -> T) -> UseState<'a, T> { - use_state(self.scope, initial_state_fn) - } -} - /// Store state between component renders! /// /// ## Dioxus equivalent of useState, designed for Rust @@ -109,6 +98,7 @@ where UseState { inner: self.inner } } } + impl Debug for UseState<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.inner.current_val) @@ -157,21 +147,21 @@ impl<'a, T: 'static> UseState<'a, T> { }) } - pub fn for_async(&self) -> AsyncUseState { - AsyncUseState { - re_render: self.inner.update_callback.clone(), - wip: self.inner.wip.clone(), - inner: self.inner.current_val.clone(), - } + pub fn for_async(self) -> UseState<'static, T> { + todo!() } - pub fn split_for_async(&'a self) -> (&'a Self, AsyncUseState) { - (self, self.for_async()) + pub fn wtih(self, f: impl FnOnce(&mut T)) { + let mut val = self.inner.wip.borrow_mut(); + + if let Some(inner) = val.as_mut() { + f(inner); + } } } impl<'a, T: 'static + ToOwned> UseState<'a, T> { - /// Gain mutable access to the new value via RefMut. + /// Gain mutable access to the new value via [`RefMut`]. /// /// If `modify` is called, then the component will re-render. /// @@ -207,57 +197,10 @@ impl<'a, T> std::ops::Deref for UseState<'a, T> { } } -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl<'a, T: Copy + Add> Add for UseState<'a, T> { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.inner.current_val.add(rhs) - } -} -impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { - fn add_assign(&mut self, rhs: T) { - self.set(self.inner.current_val.add(rhs)); - } -} -impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.inner.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { - fn sub_assign(&mut self, rhs: T) { - self.set(self.inner.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseState<'a, T> { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.inner.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseState<'a, T> { - fn mul_assign(&mut self, rhs: T) { - self.set(self.inner.current_val.mul(rhs)); - } -} -/// DIV -impl<'a, T: Copy + Div> Div for UseState<'a, T> { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.inner.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseState<'a, T> { - fn div_assign(&mut self, rhs: T) { - self.set(self.inner.current_val.div(rhs)); +// enable displaty for the handle +impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner.current_val) } } impl<'a, V, T: PartialEq> PartialEq for UseState<'a, T> { @@ -265,57 +208,10 @@ impl<'a, V, T: PartialEq> PartialEq for UseState<'a, T> { self.get() == other } } -impl<'a, O, T: Not + Copy> Not for UseState<'a, T> { +impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState<'a, T> { type Output = O; fn not(self) -> Self::Output { !*self.get() } } - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.current_val) - } -} - -/// A less ergonmic but still capable form of use_state that's valid for `static lifetime -pub struct AsyncUseState { - inner: Rc, - re_render: Rc, - wip: Rc>>, -} - -impl> AsyncUseState { - pub fn get_mut<'a>(&'a self) -> RefMut<'a, T> { - // make sure we get processed - { - let mut wip = self.wip.borrow_mut(); - if wip.is_none() { - *wip = Some(self.inner.as_ref().to_owned()); - } - (self.re_render)(); - } - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.wip.borrow_mut(), |slot| { - // - slot.as_mut().unwrap() - }) - } -} -impl AsyncUseState { - pub fn set(&mut self, val: T) { - (self.re_render)(); - *self.wip.borrow_mut() = Some(val); - } - - // pub fn get(&self) -> Ref<'_, T> { - // self.wip.borrow - // } - - pub fn get_rc(&self) -> &Rc { - &self.inner - } -} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index b00ae38f4..755508d58 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -9,14 +9,12 @@ repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] dioxus-core = { path = "../core", version = "^0.1.4" } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } - [features] default = [] serialize = ["serde", "serde_repr"] diff --git a/packages/liveview/README.md b/packages/liveview/README.md index 5bbd8b75a..1d708eab2 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -39,7 +39,7 @@ async fn order_shoes(mut req: WebsocketRequest) -> Response { dioxus::liveview::launch(App, stream).await; } -fn App(cx: Scope<()>) -> Element { +fn App(cx: Scope) -> Element { let mut count = use_state(&cx, || 0); cx.render(rsx!( button { onclick: move |_| count += 1, "Incr" } diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index ef05f2eee..22aefeac2 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] anyhow = "1.0" # cacao = { git = "https://github.com/ryanmcgrath/cacao" } -dioxus-core = { path = "../core", version ="^0.1.3"} +dioxus-core = { path = "../core", version = "^0.1.3" } log = "0.4.14" serde = "1.0.126" serde_json = "1.0.64" @@ -23,6 +23,3 @@ wry = "0.12.2" android_logger = "0.9.0" log = "0.4.11" ndk-glue = "0.2.1" - -[target.'cfg(not(target_os = "android"))'.dependencies] -simple_logger = "1.11.0" diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 938d4e957..685722718 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -20,10 +20,6 @@ use wry::{ mod dom; use dom::*; -fn init_logging() { - simple_logger::SimpleLogger::new().init().unwrap(); -} - static HTML_CONTENT: &'static str = include_str!("../../desktop/src/index.html"); pub fn launch(root: Component, builder: fn(WindowBuilder) -> WindowBuilder) -> anyhow::Result<()> { @@ -187,7 +183,6 @@ impl WebviewRenderer { } fn main() { - init_logging(); let event_loop = EventLoop::new(); let mut weviews = HashMap::new(); diff --git a/src/lib.rs b/src/lib.rs index a5e89bddb..5259377b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,7 @@ //! dioxus::desktop::launch(app); //! } //! -//! fn app(cx: Scope<()>) -> Element { +//! fn app(cx: Scope) -> Element { //! cx.render(rsx!("hello world!")) //! } //! ``` @@ -57,7 +57,7 @@ //! For components with no explicit properties, we can use the `()` type. In Dioxus, all properties are memoized by default! //! //! ```rust -//! fn App(cx: Scope<()>) -> Element { +//! fn App(cx: Scope) -> Element { //! cx.render(rsx!( //! Header { //! title: "My App", @@ -91,7 +91,7 @@ //! While components are reusable forms of UI elements, hooks are reusable forms of logic. All hooks start with `use_`. We can use hooks to declare state. //! //! ```rust -//! fn app(cx: Scope<()>) -> Element { +//! fn app(cx: Scope) -> Element { //! let name = use_state(&cx, || "world"); //! //! rsx!(cx, "hello {name}!") @@ -137,7 +137,7 @@ //! dioxus::desktop::launch(App); //! } //! -//! fn App(cx: Scope<()>) -> Element { +//! fn App(cx: Scope) -> Element { //! let mut count = use_state(&cx, || 0); //! //! cx.render(rsx!( @@ -187,20 +187,20 @@ pub use dioxus_core as core; #[cfg(feature = "hooks")] pub use dioxus_hooks as hooks; +#[cfg(feature = "router")] +pub use dioxus_router as router; + #[cfg(feature = "ssr")] pub use dioxus_ssr as ssr; #[cfg(feature = "web")] pub use dioxus_web as web; -#[cfg(feature = "mobile")] -pub use dioxus_mobile as mobile; - #[cfg(feature = "desktop")] pub use dioxus_desktop as desktop; -#[cfg(feature = "router")] -pub use dioxus_router as router; +// #[cfg(feature = "mobile")] +// pub use dioxus_mobile as mobile; pub mod events { #[cfg(feature = "html")] From 4c85bcfdc84184b4fd0fb9317ba31fe569884890 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 30 Dec 2021 03:14:47 -0500 Subject: [PATCH 5/5] awesome: arbitrary expressions excepted without braces --- README.md | 8 +- examples/README.md | 24 +-- examples/async.rs | 10 +- examples/borrowed.rs | 2 +- examples/calculator.rs | 79 ++++++---- examples/core_reference/iterators.rs | 4 +- examples/crm.rs | 177 +++++++++++----------- examples/desktop/demo.rs | 2 +- examples/desktop/kitchensink.rs | 8 - examples/desktop/tauri.rs | 1 - examples/desktop/todomvc.rs | 14 +- examples/file_explorer.rs | 73 ++++----- examples/framework_benchmark.rs | 30 ++-- examples/hydration.rs | 8 +- examples/pattern_model.rs | 40 ++--- examples/pattern_reducer.rs | 24 ++- examples/readme.rs | 6 +- examples/rsx_compile_fail.rs | 61 ++++++++ examples/rsx_usage.rs | 83 +++++++---- examples/tailwind.rs | 76 +++++----- examples/tasks.rs | 5 +- examples/todomvc.rs | 39 ++--- examples/web_tick.rs | 27 ++-- examples/webview_web.rs | 6 +- examples/xss_safety.rs | 31 ++++ packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/inlineprops.rs | 1 + packages/core-macro/src/lib.rs | 5 +- packages/core-macro/src/rsx/ambiguous.rs | 64 -------- packages/core-macro/src/rsx/body.rs | 66 --------- packages/core-macro/src/rsx/component.rs | 115 +++------------ packages/core-macro/src/rsx/element.rs | 179 +++++++++++++++-------- packages/core-macro/src/rsx/fragment.rs | 63 -------- packages/core-macro/src/rsx/mod.rs | 80 +++++++++- packages/core-macro/src/rsx/node.rs | 93 +++++++----- packages/core/src/nodes.rs | 4 +- packages/core/tests/vdom_rebuild.rs | 2 +- packages/hooks/src/usestate.rs | 64 ++++++++ packages/router/src/link.rs | 2 +- packages/ssr/src/lib.rs | 15 +- 40 files changed, 825 insertions(+), 767 deletions(-) delete mode 100644 examples/desktop/kitchensink.rs delete mode 100644 examples/desktop/tauri.rs create mode 100644 examples/rsx_compile_fail.rs create mode 100644 examples/xss_safety.rs delete mode 100644 packages/core-macro/src/rsx/ambiguous.rs delete mode 100644 packages/core-macro/src/rsx/body.rs delete mode 100644 packages/core-macro/src/rsx/fragment.rs diff --git a/README.md b/README.md index ffbfba8a6..0573fa040 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ If you know React, then you already know Dioxus. -## Examples: + + +## Examples Projects: | File Navigator (Desktop) | WiFi scanner (Desktop) | TodoMVC (All platforms) | Ecommerce w/ Tailwind (Liveview) | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -88,6 +90,10 @@ If you know React, then you already know Dioxus. See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem. +## Running examples locally + +All local examples are built for the desktop renderer. This means you can simply clone this repo and call `cargo run --example EXAMPLE_NAME`. To run non-desktop examples, checkout the example projects shown above. + ## Why Dioxus and why Rust? TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. diff --git a/examples/README.md b/examples/README.md index eb0037683..2e4959e7f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -42,14 +42,6 @@ These examples are not necessarily meant to be run, but rather serve as a refere | [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ | | [Event Listeners](./listener.rs) | Attach closures to events on elements | ✅ | -These web-specific examples must be run with `dioxus-cli` using `dioxus develop --example XYZ` - -| Example | What it does | -| ------- | ------------ | -| asd | this does | -| asd | this does | - - ## Show me some examples! @@ -75,9 +67,9 @@ fn Toggle(cx: Scope) -> Element { let mut toggled = use_state(&cx, || false); cx.render(rsx!{ div { - {&cx.props.children} + &cx.props.children button { onclick: move |_| toggled.set(true), - {toggled.and_then(|| "On").or_else(|| "Off")} + toggled.and_then(|| "On").or_else(|| "Off") } } }) @@ -112,8 +104,8 @@ fn App(cx: Scope) -> Element { if should_show { cx.render(rsx!( - {title} - ul { {list} } + title, + ul { list } )) } else { None @@ -169,10 +161,10 @@ enum Route { fn App(cx: Scope) -> Element { let route = use_router(cx, Route::parse); cx.render(rsx!(div { - {match route { + match route { Route::Home => rsx!( Home {} ), Route::Post(id) => rsx!( Post { id: id }) - }} + } })) } ``` @@ -187,8 +179,8 @@ fn App(cx: Scope) -> Element { cx.render(rsx!{ div { - "One doggo coming right up:" - {doggo} + "One doggo coming right up:", + doggo } }) } diff --git a/examples/async.rs b/examples/async.rs index e399ea016..4d292779e 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -26,20 +26,18 @@ fn app(cx: Scope) -> Element { rsx!(cx, div { h1 {"count is {count}"} - button { + button { onclick: move |_| task.stop(), "Stop counting" - onclick: move |_| task.stop() } - button { + button { onclick: move |_| task.resume(), "Start counting" - onclick: move |_| task.resume() } button { - "Switch counting direcion" onclick: move |_| { *direction.modify() *= -1; task.restart(); - } + }, + "Switch counting direcion" } }) } diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 29c502e13..6533a21a7 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -21,7 +21,7 @@ fn main() { } fn App(cx: Scope) -> Element { - let text: &mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f); + let text = cx.use_hook(|_| vec![String::from("abc=def")], |f| f); let first = text.get_mut(0).unwrap(); diff --git a/examples/calculator.rs b/examples/calculator.rs index c3814c088..e43323a21 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -24,7 +24,9 @@ fn app(cx: Scope) -> Element { let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str()); cx.render(rsx!( - div { class: "calculator", + style { [include_str!("./assets/calculator.css")] } + div { + class: "calculator", onkeydown: move |evt| match evt.key_code { KeyCode::Add => operator.set(Some("+")), KeyCode::Subtract => operator.set(Some("-")), @@ -46,12 +48,11 @@ fn app(cx: Scope) -> Element { } } _ => {} - } - div { class: "calculator-display", {[cur_val.separated_string()]} } - div { class: "input-keys" - div { class: "function-keys" + }, + div { class: "calculator-display", [cur_val.separated_string()] } + div { class: "input-keys", + div { class: "function-keys", CalculatorKey { - {[if display_value == "0" { "C" } else { "AC" }]} name: "key-clear", onclick: move |_| { display_value.set("0".to_string()); @@ -59,10 +60,10 @@ fn app(cx: Scope) -> Element { operator.set(None); cur_val.set(0.0); } - } + }, + [if display_value == "0" { "C" } else { "AC" }] } CalculatorKey { - "±" name: "key-sign", onclick: move |_| { if display_value.starts_with("-") { @@ -71,28 +72,53 @@ fn app(cx: Scope) -> Element { display_value.set(format!("-{}", *display_value)) } }, + "±" } CalculatorKey { - "%" - onclick: {toggle_percent} + onclick: toggle_percent, name: "key-percent", + "%" } } - div { class: "digit-keys" - CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" } - CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "●" } - - {(1..9).map(|k| rsx!{ - CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } - })} + div { class: "digit-keys", + CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), + "0" + } + CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), + "●" + } + (1..9).map(|k| rsx!{ + CalculatorKey { + key: "{k}", + name: "key-{k}", + onclick: move |_| input_digit(k), + "{k}" + } + }), } - div { class: "operator-keys" - CalculatorKey { name: "key-divide", onclick: move |_| operator.set(Some("/")) "÷" } - CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "×" } - CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "−" } - CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" } + + div { class: "operator-keys", + CalculatorKey { + name: "key-divide", + onclick: move |_| operator.set(Some("/")), + "÷" + } + CalculatorKey { + name: "key-multiply", + onclick: move |_| operator.set(Some("*")), + "×" + } + CalculatorKey { + name: "key-subtract", + onclick: move |_| operator.set(Some("-")), + "−" + } + CalculatorKey { + name: "key-add", + onclick: move |_| operator.set(Some("+")), + "+" + } CalculatorKey { - "=" name: "key-equals", onclick: move |_| { if let Some(op) = operator.as_ref() { @@ -109,6 +135,7 @@ fn app(cx: Scope) -> Element { operator.set(None); } }, + "=" } } } @@ -125,9 +152,9 @@ fn CalculatorKey<'a>( ) -> Element { cx.render(rsx! { button { - class: "calculator-key {name}" - onclick: {onclick} - {children} + class: "calculator-key {name}", + onclick: onclick, + children } }) } diff --git a/examples/core_reference/iterators.rs b/examples/core_reference/iterators.rs index 07452921c..ad376a1bd 100644 --- a/examples/core_reference/iterators.rs +++ b/examples/core_reference/iterators.rs @@ -20,11 +20,11 @@ pub static Example: Component = |cx| { li { onclick: move |_| example_data.set(f) "ID: {f}" ul { - {(0..10).map(|k| rsx!{ + (0..10).map(|k| rsx!{ li { "Sub iterator: {f}.{k}" } - })} + }) } } } diff --git a/examples/crm.rs b/examples/crm.rs index a8eeb456b..ca1ae19cf 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -4,7 +4,7 @@ Tiny CRM: A port of the Yew CRM example to Dioxus. use dioxus::prelude::*; fn main() { - dioxus::web::launch(App); + dioxus::desktop::launch(app); } enum Scene { ClientsList, @@ -19,88 +19,101 @@ pub struct Client { pub description: String, } -static App: Component = |cx| { - let mut clients = use_ref(&cx, || vec![] as Vec); - let mut scene = use_state(&cx, || Scene::ClientsList); +fn app(cx: Scope) -> Element { + let clients = use_ref(&cx, || vec![] as Vec); - let mut firstname = use_state(&cx, || String::new()); - let mut lastname = use_state(&cx, || String::new()); - let mut description = use_state(&cx, || String::new()); - - let scene = match *scene { - Scene::ClientsList => { - rsx!(cx, div { class: "crm" - h2 { "List of clients" margin_bottom: "10px" } - div { class: "clients" margin_left: "10px" - {clients.read().iter().map(|client| rsx!( - div { class: "client" style: "margin-bottom: 50px" - p { "First Name: {client.first_name}" } - p { "Last Name: {client.last_name}" } - p {"Description: {client.description}"} - }) - )} - } - button { class: "pure-button pure-button-primary" onclick: move |_| scene.set(Scene::NewClientForm), "Add New" } - button { class: "pure-button" onclick: move |_| scene.set(Scene::Settings), "Settings" } - }) - } - Scene::NewClientForm => { - let add_new = move |_| { - clients.write().push(Client { - description: (*description).clone(), - first_name: (*firstname).clone(), - last_name: (*lastname).clone(), - }); - description.set(String::new()); - firstname.set(String::new()); - lastname.set(String::new()); - }; - rsx!(cx, div { class: "crm" - h2 {"Add new client" margin_bottom: "10px" } - form { class: "pure-form" - input { class: "new-client firstname" placeholder: "First name" value: "{firstname}" - oninput: move |e| firstname.set(e.value.clone()) - } - input { class: "new-client lastname" placeholder: "Last name" value: "{lastname}" - oninput: move |e| lastname.set(e.value.clone()) - } - textarea { class: "new-client description" placeholder: "Description" value: "{description}" - oninput: move |e| description.set(e.value.clone()) - } - } - button { class: "pure-button pure-button-primary", onclick: {add_new}, "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), "Go Back" } - }) - } - Scene::Settings => { - rsx!(cx, div { - h2 {"Settings" margin_bottom: "10px" } - button { - background: "rgb(202, 60, 60)" - class: "pure-button pure-button-primary" - onclick: move |_| clients.write().clear(), - "Remove all clients" - } - button { - class: "pure-button pure-button-primary" - onclick: move |_| scene.set(Scene::ClientsList), - "Go Back" - } - }) - } - }; + let scene = use_state(&cx, || Scene::ClientsList); + let firstname = use_state(&cx, String::new); + let lastname = use_state(&cx, String::new); + let description = use_state(&cx, String::new); cx.render(rsx!( - body { - link { - rel: "stylesheet" - href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css" - integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" - crossorigin: "anonymous" - } - margin_left: "35%" - h1 {"Dioxus CRM Example"} - {scene} - } + body { margin_left: "35%", + link { + rel: "stylesheet", + href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css", + integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", + crossorigin: "anonymous", + } + + h1 {"Dioxus CRM Example"} + + match *scene { + Scene::ClientsList => rsx!( + div { class: "crm", + h2 { margin_bottom: "10px", "List of clients" } + div { class: "clients", margin_left: "10px", + clients.read().iter().map(|client| rsx!( + div { class: "client", style: "margin-bottom: 50px", + p { "First Name: {client.first_name}" } + p { "Last Name: {client.last_name}" } + p {"Description: {client.description}"} + }) + ) + } + button { class: "pure-button pure-button-primary", onclick: move |_| scene.set(Scene::NewClientForm), "Add New" } + button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" } + } + ), + Scene::NewClientForm => rsx!( + div { class: "crm", + h2 { margin_bottom: "10px", "Add new client" } + form { class: "pure-form", + input { + class: "new-client firstname", + placeholder: "First name", + value: "{firstname}", + oninput: move |e| firstname.set(e.value.clone()) + } + input { + class: "new-client lastname", + placeholder: "Last name", + value: "{lastname}", + oninput: move |e| lastname.set(e.value.clone()) + } + textarea { + class: "new-client description", + placeholder: "Description", + value: "{description}", + oninput: move |e| description.set(e.value.clone()) + } + } + button { + class: "pure-button pure-button-primary", + onclick: move |_| { + clients.write().push(Client { + description: (*description).clone(), + first_name: (*firstname).clone(), + last_name: (*lastname).clone(), + }); + description.set(String::new()); + firstname.set(String::new()); + lastname.set(String::new()); + }, + "Add New" + } + button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), + "Go Back" + } + } + ), + Scene::Settings => rsx!( + div { + h2 { margin_bottom: "10px", "Settings" } + button { + background: "rgb(202, 60, 60)", + class: "pure-button pure-button-primary", + onclick: move |_| clients.write().clear(), + "Remove all clients" + } + button { + class: "pure-button pure-button-primary", + onclick: move |_| scene.set(Scene::ClientsList), + "Go Back" + } + } + ) + } + } )) -}; +} diff --git a/examples/desktop/demo.rs b/examples/desktop/demo.rs index 1782d8cb0..d8dc51697 100644 --- a/examples/desktop/demo.rs +++ b/examples/desktop/demo.rs @@ -13,6 +13,6 @@ static App: Component = |cx| { div { "hello world!" } - {(0..10).map(|f| rsx!( div {"abc {f}"}))} + (0..10).map(|f| rsx!( div {"abc {f}"})) )) }; diff --git a/examples/desktop/kitchensink.rs b/examples/desktop/kitchensink.rs deleted file mode 100644 index 5ec049ee9..000000000 --- a/examples/desktop/kitchensink.rs +++ /dev/null @@ -1,8 +0,0 @@ -use dioxus_core as dioxus; -use dioxus_core::prelude::*; -use dioxus_core_macro::*; -use dioxus_hooks::*; - -use dioxus_html as dioxus_elements; - -fn main() {} diff --git a/examples/desktop/tauri.rs b/examples/desktop/tauri.rs deleted file mode 100644 index f328e4d9d..000000000 --- a/examples/desktop/tauri.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/examples/desktop/todomvc.rs b/examples/desktop/todomvc.rs index e70ed1806..46ee5ec1d 100644 --- a/examples/desktop/todomvc.rs +++ b/examples/desktop/todomvc.rs @@ -98,24 +98,24 @@ pub static App: Component = |cx| { } } } - ul { class: "todo-list" - {filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id }))} + ul { class: "todo-list", + filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id })) } - {(!todos.read().is_empty()).then(|| rsx!( - footer { class: "footer" + (!todos.read().is_empty()).then(|| rsx!( + footer { class: "footer", span { class: "todo-count" strong {"{items_left} "} span {"{item_text} left"} } ul { class: "filters" li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} } - {(show_clear_completed).then(|| rsx!( + (show_clear_completed).then(|| rsx!( button { class: "clear-completed", onclick: move |_| clear_completed(), "Clear completed" } - ))} + )) } - ))} + )) } } footer { class: "info" diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 05f5c4ca7..f27e018a9 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -4,50 +4,45 @@ //! This is a fun little desktop application that lets you explore the file system. //! //! This example is interesting because it's mixing filesystem operations and GUI, which is typically hard for UI to do. +//! +//! It also uses `use_ref` to maintain a model, rather than `use_state`. That way, +//! we dont need to clutter our code with `read` commands. use dioxus::prelude::*; fn main() { - dioxus::desktop::launch_cfg(App, |c| { + dioxus::desktop::launch_cfg(app, |c| { c.with_window(|w| { - w.with_resizable(true).with_inner_size( - dioxus::desktop::wry::application::dpi::LogicalSize::new(400.0, 800.0), - ) + w.with_resizable(true) + .with_inner_size(dioxus::desktop::tao::dpi::LogicalSize::new(400.0, 800.0)) }) }); } -static App: Component = |cx| { - let file_manager = use_ref(&cx, Files::new); - let files = file_manager.read(); - - let file_list = files.path_names.iter().enumerate().map(|(dir_id, path)| { - rsx! ( - li { a {"{path}", onclick: move |_| file_manager.write().enter_dir(dir_id), href: "#"} } - ) - }); - - let err_disp = files.err.as_ref().map(|err| { - rsx! ( - div { - code {"{err}"} - button {"x", onclick: move |_| file_manager.write().clear_err() } - } - ) - }); - - let current_dir = files.current(); +fn app(cx: Scope) -> Element { + let files = use_ref(&cx, Files::new); cx.render(rsx!( - div { - h1 {"Files: "} - h3 {"Cur dir: {current_dir}"} - button { "go up", onclick: move |_| file_manager.write().go_up() } - ol { {file_list} } - {err_disp} + h1 { "Files: " } + h3 { "Cur dir: " [files.read().current()] } + button { onclick: move |_| files.write().go_up(), "go up" } + ol { + files.read().path_names.iter().enumerate().map(|(dir_id, path)| rsx!( + li { key: "{path}", + a { href: "#", onclick: move |_| files.write().enter_dir(dir_id), + "{path}", + } + } + )) } + files.read().err.as_ref().map(|err| rsx!( + div { + code { "{err}" } + button { onclick: move |_| files.write().clear_err(), "x" } + } + )) )) -}; +} struct Files { path_stack: Vec, @@ -69,29 +64,21 @@ impl Files { } fn reload_path_list(&mut self) { - let cur_path = self.path_stack.last().unwrap(); - log::info!("Reloading path list for {:?}", cur_path); - let paths = match std::fs::read_dir(cur_path) { + let paths = match std::fs::read_dir(self.path_stack.last().unwrap()) { Ok(e) => e, Err(err) => { - let err = format!("An error occured: {:?}", err); - self.err = Some(err); + self.err = Some(format!("An error occured: {:?}", err)); self.path_stack.pop(); return; } }; - let collected = paths.collect::>(); - log::info!("Path list reloaded {:#?}", collected); // clear the current state self.clear_err(); self.path_names.clear(); - for path in collected { - self.path_names - .push(path.unwrap().path().display().to_string()); - } - log::info!("path namees are {:#?}", self.path_names); + self.path_names + .extend(paths.map(|path| path.unwrap().path().display().to_string())); } fn go_up(&mut self) { diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 100169679..f09693f1b 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -2,8 +2,8 @@ use dioxus::prelude::*; use rand::prelude::*; fn main() { - dioxus::web::launch(App); - // dioxus::desktop::launch(App); + // dioxus::web::launch(App); + dioxus::desktop::launch(App); } #[derive(Clone, PartialEq)] @@ -31,16 +31,16 @@ impl Label { } static App: Component = |cx| { - let mut items = use_ref(&cx, || vec![]); - let mut selected = use_state(&cx, || None); + let items = use_ref(&cx, || vec![]); + let selected = use_state(&cx, || None); cx.render(rsx! { - div { class: "container" - div { class: "jumbotron" - div { class: "row" + div { class: "container", + div { class: "jumbotron", + div { class: "row", div { class: "col-md-6", h1 { "Dioxus" } } - div { class: "col-md-6" - div { class: "row" + div { class: "col-md-6", + div { class: "row", ActionButton { name: "Create 1,000 rows", id: "run", onclick: move || items.set(Label::new_list(1_000)), } @@ -67,15 +67,15 @@ static App: Component = |cx| { tbody { {items.read().iter().enumerate().map(|(id, item)| { let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""}; - rsx!(tr { class: "{is_in_danger}" + rsx!(tr { class: "{is_in_danger}", td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), - a { class: "lbl", {item.labels} } + a { class: "lbl", item.labels } } - td { class: "col-md-1" + td { class: "col-md-1", a { class: "remove", onclick: move |_| { items.write().remove(id); }, - span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } + span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" } } } td { class: "col-md-6" } @@ -83,7 +83,7 @@ static App: Component = |cx| { })} } } - // span { class: "preloadicon glyphicon glyphicon-remove" aria_hidden: "true" } + span { class: "preloadicon glyphicon glyphicon-remove", aria_hidden: "true" } } }) }; @@ -96,7 +96,7 @@ struct ActionButtonProps<'a> { } fn ActionButton<'a>(cx: Scope<'a, ActionButtonProps<'a>>) -> Element { - rsx!(cx, div { class: "col-sm-6 smallpad" + rsx!(cx, div { class: "col-sm-6 smallpad", button { class:"btn btn-primary btn-block", r#type: "button", id: "{cx.props.id}", onclick: move |_| (cx.props.onclick)(), "{cx.props.name}" } diff --git a/examples/hydration.rs b/examples/hydration.rs index 3c4faa55d..1f769e799 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -16,18 +16,18 @@ fn main() { let vdom = VirtualDom::new(App); let content = ssr::render_vdom_cfg(&vdom, |f| f.pre_render(true)); - dioxus::desktop::launch_cfg(App, |c| c.with_prerendered(content)); + // dioxus::desktop::launch_cfg(App, |c| c.with_prerendered(content)); } static App: Component = |cx| { - let mut val = use_state(&cx, || 0); + let val = use_state(&cx, || 0); cx.render(rsx! { div { - h1 {"hello world. Count: {val}"} + h1 { "hello world. Count: {val}" } button { + onclick: move |_| *val.modify() += 1, "click to increment" - onclick: move |_| val += 1 } } }) diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index 252a000c9..45b0ccf4e 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -24,42 +24,42 @@ use dioxus::prelude::*; const STYLE: &str = include_str!("./assets/calculator.css"); fn main() { env_logger::init(); - dioxus::desktop::launch_cfg(App, |cfg| { - cfg.with_window(|w| { - w.with_title("Calculator Demo") - .with_resizable(false) - .with_inner_size(LogicalSize::new(320.0, 530.0)) - }) - }); + // dioxus::desktop::launch_cfg(App, |cfg| { + // cfg.with_window(|w| { + // w.with_title("Calculator Demo") + // .with_resizable(false) + // .with_inner_size(LogicalSize::new(320.0, 530.0)) + // }) + // }); } -static App: Component = |cx| { +fn app(cx: Scope) -> Element { let state = use_ref(&cx, || Calculator::new()); let clear_display = state.read().display_value.eq("0"); let clear_text = if clear_display { "C" } else { "AC" }; let formatted = state.read().formatted_display(); - rsx!(cx, div { id: "wrapper" + rsx!(cx, div { id: "wrapper", div { class: "app", style { "{STYLE}" } div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt), div { class: "calculator-display", "{formatted}"} - div { class: "calculator-keypad" - div { class: "input-keys" - div { class: "function-keys" + div { class: "calculator-keypad", + div { class: "input-keys", + div { class: "function-keys", CalculatorKey { name: "key-clear", onclick: move |_| state.write().clear_display(), "{clear_text}" } CalculatorKey { name: "key-sign", onclick: move |_| state.write().toggle_sign(), "±"} CalculatorKey { name: "key-percent", onclick: move |_| state.write().toggle_percent(), "%"} } - div { class: "digit-keys" + div { class: "digit-keys", CalculatorKey { name: "key-0", onclick: move |_| state.write().input_digit(0), "0" } CalculatorKey { name: "key-dot", onclick: move |_| state.write().input_dot(), "●" } - {(1..10).map(move |k| rsx!{ + (1..10).map(move |k| rsx!{ CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.write().input_digit(k), "{k}" } - })} + }) } } - div { class: "operator-keys" + div { class: "operator-keys", CalculatorKey { name:"key-divide", onclick: move |_| state.write().set_operator(Operator::Div), "÷" } CalculatorKey { name:"key-multiply", onclick: move |_| state.write().set_operator(Operator::Mul), "×" } CalculatorKey { name:"key-subtract", onclick: move |_| state.write().set_operator(Operator::Sub), "−" } @@ -70,7 +70,7 @@ static App: Component = |cx| { } } }) -}; +} #[derive(Props)] struct CalculatorKeyProps<'a> { @@ -82,9 +82,9 @@ struct CalculatorKeyProps<'a> { fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element { cx.render(rsx! { button { - class: "calculator-key {cx.props.name}" - onclick: move |e| (cx.props.onclick)(e) - {&cx.props.children} + class: "calculator-key {cx.props.name}", + onclick: move |e| (cx.props.onclick)(e), + &cx.props.children } }) } diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index cd14460f6..26bf3acff 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -14,20 +14,18 @@ fn main() { pub static App: Component = |cx| { let state = use_state(&cx, PlayerState::new); - let is_playing = state.is_playing(); - - rsx!(cx, div { - h1 {"Select an option"} - h3 {"The radio is... {is_playing}!"} - button { - "Pause" - onclick: move |_| state.modify().reduce(PlayerAction::Pause) + cx.render(rsx!( + div { + h1 {"Select an option"} + h3 { "The radio is... " [state.is_playing()], "!" } + button { onclick: move |_| state.modify().reduce(PlayerAction::Pause), + "Pause" + } + button { onclick: move |_| state.modify().reduce(PlayerAction::Play), + "Play" + } } - button { - "Play" - onclick: move |_| state.modify().reduce(PlayerAction::Play) - } - }) + )) }; enum PlayerAction { diff --git a/examples/readme.rs b/examples/readme.rs index 9f0a288e3..ae17c1cd9 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } static App: Component = |cx| { - let count = use_state(&cx, || 0); + let mut count = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| *count.modify() += 1, "Up high!" } - button { onclick: move |_| *count.modify() -= 1, "Down low!" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } } }) }; diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs new file mode 100644 index 000000000..c95f3d7fe --- /dev/null +++ b/examples/rsx_compile_fail.rs @@ -0,0 +1,61 @@ +use dioxus::prelude::*; + +fn main() { + let mut vdom = VirtualDom::new(example); + vdom.rebuild(); + + let out = dioxus::ssr::render_vdom_cfg(&vdom, |c| c.newline(true).indent(true)); + println!("{}", out); +} + +fn example(cx: Scope) -> Element { + let items = use_state(&cx, || { + vec![Thing { + a: "asd".to_string(), + b: 10, + }] + }); + + let things = use_ref(&cx, || { + vec![Thing { + a: "asd".to_string(), + b: 10, + }] + }); + let things_list = things.read(); + + let mything = use_ref(&cx, || Some(String::from("asd"))); + let mything_read = mything.read(); + + cx.render(rsx!( + div { + div { + id: "asd", + "your neighborhood spiderman" + + items.iter().cycle().take(5).map(|f| rsx!{ + div { + "{f.a}" + } + }) + + things_list.iter().map(|f| rsx!{ + div { + "{f.a}" + } + }) + + mything_read.as_ref().map(|f| rsx!{ + div { + "{f}" + } + }) + } + } + )) +} + +struct Thing { + a: String, + b: u32, +} diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 195edac00..04afb7407 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -60,6 +60,7 @@ pub static EXAMPLE: Component = |cx| { h1 {"Some text"} h1 {"Some text with {formatting}"} h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} + h1 {"Formatting without interpolation " [formatting_tuple.0] "and" [formatting_tuple.1] } h2 { "Multiple" "Text" @@ -72,7 +73,7 @@ pub static EXAMPLE: Component = |cx| { h3 {"elements"} } div { - class: "my special div" + class: "my special div", h1 {"Headers and attributes!"} } div { @@ -88,42 +89,51 @@ pub static EXAMPLE: Component = |cx| { } // Expressions can be used in element position too: - {rsx!(p { "More templating!" })} - // {html!(

"Even HTML templating!!"

)} + rsx!(p { "More templating!" }), // Iterators - {(0..10).map(|i| rsx!(li { "{i}" }))} - {{ + (0..10).map(|i| rsx!(li { "{i}" })), + + // Iterators within expressions + { let data = std::collections::HashMap::<&'static str, &'static str>::new(); // Iterators *should* have keys when you can provide them. // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. // Using an "ID" associated with your data is a good idea. - data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) - }} + data.into_iter().map(|(k, v)| rsx!(li { key: "{k}", "{v}" })) + } // Matching - {match true { + match true { true => rsx!( h1 {"Top text"}), false => rsx!( h1 {"Bottom text"}) - }} + } // Conditional rendering // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. // You can convert a bool condition to rsx! with .then and .or - {true.then(|| rsx!(div {}))} + true.then(|| rsx!(div {})), - // True conditions need to be rendered (same reasons as matching) - {if true { - rsx!(cx, h1 {"Top text"}) + // Alternatively, you can use the "if" syntax - but both branches must be resolve to Element + if false { + rsx!(h1 {"Top text"}) } else { - rsx!(cx, h1 {"Bottom text"}) - }} + rsx!(h1 {"Bottom text"}) + } - // returning "None" is a bit noisy... but rare in practice - {None as Option<()>} + // Using optionals for diverging branches + if true { + Some(rsx!(h1 {"Top text"})) + } else { + None + } + + + // returning "None" without a diverging branch is a bit noisy... but rare in practice + None as Option<()>, // Use the Dioxus type-alias for less noise - {NONE_ELEMENT} + NONE_ELEMENT, // can also just use empty fragments Fragment {} @@ -137,9 +147,8 @@ pub static EXAMPLE: Component = |cx| { Fragment { "D" Fragment { - "heavily nested fragments is an antipattern" - "they cause Dioxus to do unnecessary work" - "don't use them carelessly if you can help it" + "E" + "F" } } } @@ -158,22 +167,29 @@ pub static EXAMPLE: Component = |cx| { Taller { a: "asd" } // Can pass in props directly as an expression - {{ + { let props = TallerProps {a: "hello", children: Default::default()}; rsx!(Taller { ..props }) - }} + } // Spreading can also be overridden manually Taller { - ..TallerProps { a: "ballin!", children: Default::default() } + ..TallerProps { a: "ballin!", children: Default::default() }, a: "not ballin!" } // Can take children too! Taller { a: "asd", div {"hello world!"} } + // Components can be used with the `call` syntax + // This component's props are defined *inline* with the `inline_props` macro + with_inline( + text: "using functionc all syntax" + ) + // helper functions - {helper(&cx, "hello world!")} + // Single values must be wrapped in braces or `Some` to satisfy `IntoIterator` + [helper(&cx, "hello world!")] } }) }; @@ -187,6 +203,7 @@ mod baller { #[derive(Props, PartialEq)] pub struct BallerProps {} + #[allow(non_snake_case)] /// This component totally balls pub fn Baller(_: Scope) -> Element { todo!() @@ -195,12 +212,20 @@ mod baller { #[derive(Props)] pub struct TallerProps<'a> { + /// Fields are documented and accessible in rsx! a: &'static str, children: Element<'a>, } -/// This component is taller than most :) -pub fn Taller<'a>(_: Scope<'a, TallerProps<'a>>) -> Element { - let b = true; - todo!() +/// Documention for this component is visible within the rsx macro +#[allow(non_snake_case)] +pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element { + cx.render(rsx! { + &cx.props.children + }) +} + +#[inline_props] +fn with_inline<'a>(cx: Scope<'a>, text: &'a str) -> Element { + rsx!(cx, p { "{text}" }) } diff --git a/examples/tailwind.rs b/examples/tailwind.rs index 49da99ee8..9bd87c15e 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -16,9 +16,9 @@ const STYLE: &str = "body {overflow:hidden;}"; pub static App: Component = |cx| { cx.render(rsx!( - div { class: "overflow-hidden" + div { class: "overflow-hidden", style { "{STYLE}" } - link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel:"stylesheet" } + link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } Header {} Entry {} Hero {} @@ -33,20 +33,20 @@ pub static App: Component = |cx| { pub static Header: Component = |cx| { cx.render(rsx! { div { - header { class: "text-gray-400 bg-gray-900 body-font" - div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center" - a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0" + header { class: "text-gray-400 bg-gray-900 body-font", + div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", + a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", StacksIcon {} - span { class: "ml-3 text-xl" "Hello Dioxus!"} + span { class: "ml-3 text-xl", "Hello Dioxus!"} } - nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" - a { class: "mr-5 hover:text-white" "First Link"} - a { class: "mr-5 hover:text-white" "Second Link"} - a { class: "mr-5 hover:text-white" "Third Link"} - a { class: "mr-5 hover:text-white" "Fourth Link"} + nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center", + a { class: "mr-5 hover:text-white", "First Link"} + a { class: "mr-5 hover:text-white", "Second Link"} + a { class: "mr-5 hover:text-white", "Third Link"} + a { class: "mr-5 hover:text-white", "Fourth Link"} } button { - class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0" + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", "Button" RightArrowIcon {} } @@ -59,34 +59,34 @@ pub static Header: Component = |cx| { pub static Hero: Component = |cx| { // cx.render(rsx! { - section{ class: "text-gray-400 bg-gray-900 body-font" - div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center" - div { class: "lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center" - h1 { class: "title-font sm:text-4xl text-3xl mb-4 font-medium text-white" + section{ class: "text-gray-400 bg-gray-900 body-font", + div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center", + div { class: "lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center", + h1 { class: "title-font sm:text-4xl text-3xl mb-4 font-medium text-white", br { class: "hidden lg:inline-block" } "Dioxus Sneak Peek" } p { - class: "mb-8 leading-relaxed" + class: "mb-8 leading-relaxed", "Dioxus is a new UI framework that makes it easy and simple to write cross-platform apps using web technologies! It is functional, fast, and portable. Dioxus can run on the web, on the desktop, and on mobile and embedded platforms." } - div { class: "flex justify-center" + div { class: "flex justify-center", button { - class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg" + class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded text-lg", "Learn more" } button { - class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-none hover:bg-gray-700 hover:text-white rounded text-lg" + class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-none hover:bg-gray-700 hover:text-white rounded text-lg", "Build an app" } } } - div { class: "lg:max-w-lg lg:w-full md:w-1/2 w-5/6" - img { class: "object-cover object-center rounded" alt: "hero" src: "https://i.imgur.com/oK6BLtw.png" + div { class: "lg:max-w-lg lg:w-full md:w-1/2 w-5/6", + img { class: "object-cover object-center rounded", alt: "hero", src: "https://i.imgur.com/oK6BLtw.png", referrerpolicy:"no-referrer" } } @@ -97,8 +97,8 @@ pub static Hero: Component = |cx| { pub static Entry: Component = |cx| { // cx.render(rsx! { - section{ class: "text-gray-400 bg-gray-900 body-font" - div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center" + section{ class: "text-gray-400 bg-gray-900 body-font", + div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center", textarea { } @@ -111,13 +111,13 @@ pub static StacksIcon: Component = |cx| { cx.render(rsx!( svg { // xmlns: "http://www.w3.org/2000/svg" - fill: "none" - stroke: "currentColor" - stroke_linecap: "round" - stroke_linejoin: "round" - stroke_width: "2" - class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full" - view_box: "0 0 24 24" + fill: "none", + stroke: "currentColor", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full", + view_box: "0 0 24 24", path { d: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"} } )) @@ -125,13 +125,13 @@ pub static StacksIcon: Component = |cx| { pub static RightArrowIcon: Component = |cx| { cx.render(rsx!( svg { - fill: "none" - stroke: "currentColor" - stroke_linecap: "round" - stroke_linejoin: "round" - stroke_width: "2" - class: "w-4 h-4 ml-1" - view_box: "0 0 24 24" + fill: "none", + stroke: "currentColor", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + class: "w-4 h-4 ml-1", + view_box: "0 0 24 24", path { d: "M5 12h14M12 5l7 7-7 7"} } )) diff --git a/examples/tasks.rs b/examples/tasks.rs index a378c24a6..0838c1fa2 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -15,8 +15,9 @@ fn app(cx: Scope) -> Element { use_future(&cx, || { for_async![count]; async move { - while let _ = tokio::time::sleep(Duration::from_millis(1000)).await { - *count.modify() += 1; + loop { + tokio::time::sleep(Duration::from_millis(1000)).await; + count += 1; } } }); diff --git a/examples/todomvc.rs b/examples/todomvc.rs index a41f78d5e..8a62e0ddd 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -29,7 +29,7 @@ const App: Component = |cx| { let todolist = todos .iter() - .filter(|(id, item)| match *filter { + .filter(|(_id, item)| match *filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -48,34 +48,37 @@ const App: Component = |cx| { _ => "items", }; - rsx!(cx, div { id: "app" + rsx!(cx, div { id: "app", style {"{STYLE}"} div { - header { class: "header" + header { class: "header", h1 {"todos"} input { - class: "new-todo" - placeholder: "What needs to be done?" - value: "{draft}" - oninput: move |evt| draft.set(evt.value.clone()) + class: "new-todo", + placeholder: "What needs to be done?", + value: "{draft}", + oninput: move |evt| draft.set(evt.value.clone()), } } - {todolist} - {(!todos.is_empty()).then(|| rsx!( + todolist, + (!todos.is_empty()).then(|| rsx!( footer { - span { strong {"{items_left}"} span {"{item_text} left"} } - ul { class: "filters" + span { + strong {"{items_left}"} + span {"{item_text} left"} + } + ul { class: "filters", li { class: "All", a { href: "", onclick: move |_| filter.set(FilterState::All), "All" }} li { class: "Active", a { href: "active", onclick: move |_| filter.set(FilterState::Active), "Active" }} li { class: "Completed", a { href: "completed", onclick: move |_| filter.set(FilterState::Completed), "Completed" }} } } - ))} + )) } - footer { class: "info" + footer { class: "info", p {"Double-click to edit a todo"} - p { "Created by ", a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" }} - p { "Part of ", a { "TodoMVC", href: "http://todomvc.com" }} + p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} + p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} } }) }; @@ -93,13 +96,13 @@ pub fn TodoEntry(cx: Scope) -> Element { rsx!(cx, li { "{todo.id}" input { - class: "toggle" - r#type: "checkbox" + class: "toggle", + r#type: "checkbox", "{todo.checked}" } {is_editing.then(|| rsx!{ input { - value: "{contents}" + value: "{contents}", oninput: move |evt| contents.set(evt.value.clone()) } })} diff --git a/examples/web_tick.rs b/examples/web_tick.rs index 447bd1121..74cc1897f 100644 --- a/examples/web_tick.rs +++ b/examples/web_tick.rs @@ -13,28 +13,19 @@ use dioxus::prelude::*; fn main() { - #[cfg(target_arch = "wasm32")] - intern_strings(); - - dioxus::web::launch(App); + dioxus::desktop::launch(App); } static App: Component = |cx| { let mut rng = SmallRng::from_entropy(); - let rows = (0..1_000).map(|f| { - let label = Label::new(&mut rng); - rsx! { - Row { - row_id: f, - label: label - } - } - }); cx.render(rsx! { table { tbody { - {rows} + (0..1_000).map(|f| { + let label = Label::new(&mut rng); + rsx! (Row { row_id: f, label: label }) + }) } } }) @@ -50,12 +41,12 @@ fn Row(cx: Scope) -> Element { cx.render(rsx! { tr { td { class:"col-md-1", "{cx.props.row_id}" } - td { class:"col-md-1", onclick: move |_| { /* run onselect */ } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ }, a { class: "lbl", "{adj}" "{col}" "{noun}" } } - td { class: "col-md-1" - a { class: "remove", onclick: move |_| {/* remove */} - span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } + td { class: "col-md-1", + a { class: "remove", onclick: move |_| {/* remove */}, + span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" } } } td { class: "col-md-6" } diff --git a/examples/webview_web.rs b/examples/webview_web.rs index 5a7df845d..aa762cb88 100644 --- a/examples/webview_web.rs +++ b/examples/webview_web.rs @@ -16,13 +16,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let mut count = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "Hifive counter: {count}" } - button { onclick: move |_| *count.modify() += 1, "Up high!" } - button { onclick: move |_| *count.modify() -= 1, "Down low!" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } } }) } diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs new file mode 100644 index 000000000..cdb0705ca --- /dev/null +++ b/examples/xss_safety.rs @@ -0,0 +1,31 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let contents = use_state(&cx, || String::from("")); + + cx.render(rsx! { + div { + "hello world!" + + h1 { + "{contents}" + } + h3 { + [contents.as_str()] + } + + input { + value: "{contents}", + oninput: move |e| { + contents.set(e.value.clone()); + eprintln!("asd"); + }, + "type": "text", + } + } + }) +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index e3179510e..9af9f3126 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -16,6 +16,7 @@ proc-macro = true [dependencies] once_cell = "1.8" +proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index 64dab1ee3..acbf778d3 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -99,6 +99,7 @@ impl ToTokens for InlinePropsBody { }; out_tokens.append_all(quote! { + #[allow(non_camel_case)] #modifiers #vis struct #struct_name #generics { #(#fields),* diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 2d17f4bbc..8f603bbb7 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -177,11 +177,12 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// todo!() /// } /// ``` +#[proc_macro_error::proc_macro_error] #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { match syn::parse::(s) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), + Err(err) => err.to_compile_error().into(), + Ok(stream) => stream.to_token_stream().into(), } } diff --git a/packages/core-macro/src/rsx/ambiguous.rs b/packages/core-macro/src/rsx/ambiguous.rs deleted file mode 100644 index 16cb2c4e5..000000000 --- a/packages/core-macro/src/rsx/ambiguous.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Parse anything that has a pattern of < Ident, Bracket > -//! ======================================================== -//! -//! Whenever a `name {}` pattern emerges, we need to parse it into an element, a component, or a fragment. -//! This feature must support: -//! - Namepsaced/pathed components -//! - Differentiating between built-in and custom elements - -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; -use syn::{ - parse::{Parse, ParseStream}, - Error, Ident, Result, Token, -}; - -#[allow(clippy::large_enum_variant)] -pub enum AmbiguousElement { - Element(Element), - Component(Component), -} - -impl Parse for AmbiguousElement { - fn parse(input: ParseStream) -> Result { - // Try to parse as an absolute path and immediately defer to the componetn - if input.peek(Token![::]) { - return input.parse::().map(AmbiguousElement::Component); - } - - // If not an absolute path, then parse the ident and check if it's a valid tag - if let Ok(pat) = input.fork().parse::() { - if pat.segments.len() > 1 { - return input.parse::().map(AmbiguousElement::Component); - } - } - - use syn::ext::IdentExt; - if let Ok(name) = input.fork().call(Ident::parse_any) { - let name_str = name.to_string(); - - let first_char = name_str.chars().next().unwrap(); - if first_char.is_ascii_uppercase() { - input.parse::().map(AmbiguousElement::Component) - } else { - if input.peek2(syn::token::Paren) { - input.parse::().map(AmbiguousElement::Component) - } else { - input.parse::().map(AmbiguousElement::Element) - } - } - } else { - Err(Error::new(input.span(), "Not a valid Html tag")) - } - } -} -impl ToTokens for AmbiguousElement { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self { - AmbiguousElement::Element(el) => el.to_tokens(tokens), - AmbiguousElement::Component(comp) => comp.to_tokens(tokens), - } - } -} diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs deleted file mode 100644 index 83d2886e6..000000000 --- a/packages/core-macro/src/rsx/body.rs +++ /dev/null @@ -1,66 +0,0 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - Ident, Result, Token, -}; - -use super::*; - -pub struct CallBody { - custom_context: Option, - roots: Vec, -} - -/// The custom rusty variant of parsing rsx! -impl Parse for CallBody { - fn parse(input: ParseStream) -> Result { - let custom_context = try_parse_custom_context(input)?; - let (_, roots, _) = BodyConfig::new_call_body().parse_component_body(input)?; - Ok(Self { - custom_context, - roots, - }) - } -} - -fn try_parse_custom_context(input: ParseStream) -> Result> { - let res = if input.peek(Ident) && input.peek2(Token![,]) { - let name = input.parse::()?; - input.parse::()?; - Some(name) - } else { - None - }; - Ok(res) -} - -/// Serialize the same way, regardless of flavor -impl ToTokens for CallBody { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let inner = if self.roots.len() == 1 { - let inner = &self.roots[0]; - quote! {#inner} - } else { - let childs = &self.roots; - quote! { __cx.fragment_root([ #(#childs),* ]) } - }; - - match &self.custom_context { - // The `in cx` pattern allows directly rendering - Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - })) - }), - // Otherwise we just build the LazyNode wrapper - None => out_tokens.append_all(quote! { - LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - }) - }), - }; - } -} diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 9c51cb748..a19f55bac 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -19,11 +19,10 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Error, Expr, ExprClosure, Ident, Result, Token, + token, Expr, ExprClosure, Ident, Result, Token, }; pub struct Component { - // accept any path-like argument name: syn::Path, body: Vec, children: Vec, @@ -32,12 +31,8 @@ pub struct Component { impl Parse for Component { fn parse(stream: ParseStream) -> Result { - // let name = s.parse::()?; - // todo: look into somehow getting the crate/super/etc - let name = syn::Path::parse_mod_style(stream)?; - // parse the guts let content: ParseBuffer; // if we see a `{` then we have a block @@ -48,13 +43,25 @@ impl Parse for Component { syn::parenthesized!(content in stream); } - let cfg: BodyConfig = BodyConfig { - allow_children: true, - allow_fields: true, - allow_manual_props: true, - }; + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; - let (body, children, manual_props) = cfg.parse_component_body(&content)?; + while !content.is_empty() { + // if we splat into a component then we're merging properties + if content.peek(Token![..]) { + content.parse::()?; + manual_props = Some(content.parse::()?); + } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + body.push(content.parse::()?); + } else { + children.push(content.parse::()?); + } + + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } Ok(Self { name, @@ -65,77 +72,6 @@ impl Parse for Component { } } -pub struct BodyConfig { - pub allow_fields: bool, - pub allow_children: bool, - pub allow_manual_props: bool, -} - -impl BodyConfig { - /// The configuration to parse the root - pub fn new_call_body() -> Self { - Self { - allow_children: true, - allow_fields: false, - allow_manual_props: false, - } - } -} - -impl BodyConfig { - // todo: unify this body parsing for both elements and components - // both are style rather ad-hoc, though components are currently more configured - pub fn parse_component_body( - &self, - content: &ParseBuffer, - ) -> Result<(Vec, Vec, Option)> { - let mut body = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - - 'parsing: loop { - // [1] Break if empty - if content.is_empty() { - break 'parsing; - } - - if content.peek(Token![..]) { - if !self.allow_manual_props { - return Err(Error::new( - content.span(), - "Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.", - )); - } - content.parse::()?; - manual_props = Some(content.parse::()?); - } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - if !self.allow_fields { - return Err(Error::new( - content.span(), - "Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.", - )); - } - body.push(content.parse::()?); - } else { - if !self.allow_children { - return Err(Error::new( - content.span(), - "This item is not allowed to accept children.", - )); - } - children.push(content.parse::()?); - } - - // consume comma if it exists - // we don't actually care if there *are* commas between attrs - if content.peek(Token![,]) { - let _ = content.parse::(); - } - } - Ok((body, children, manual_props)) - } -} - impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; @@ -219,9 +155,7 @@ pub struct ComponentField { enum ContentField { ManExpr(Expr), - OnHandler(ExprClosure), - // A handler was provided in {} tokens OnHandlerRaw(Expr), } @@ -229,9 +163,6 @@ impl ToTokens for ContentField { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { ContentField::ManExpr(e) => e.to_tokens(tokens), - ContentField::OnHandler(e) => tokens.append_all(quote! { - __cx.bump().alloc(#e) - }), ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { __cx.bump().alloc(#e) }), @@ -246,13 +177,7 @@ impl Parse for ComponentField { let name_str = name.to_string(); let content = if name_str.starts_with("on") { - if input.peek(token::Brace) { - let content; - syn::braced!(content in input); - ContentField::OnHandlerRaw(content.parse()?) - } else { - ContentField::OnHandler(input.parse()?) - } + ContentField::OnHandlerRaw(input.parse()?) } else { ContentField::ManExpr(input.parse::()?) }; diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index babaee825..cb0b7bdaa 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, ExprClosure, Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -33,45 +33,69 @@ impl Parse for Element { let mut key = None; let mut _el_ref = None; - // todo: more descriptive error handling - while !content.is_empty() { + // parse fields with commas + // break when we don't get this pattern anymore + // start parsing bodynodes + // "def": 456, + // abc: 123, + loop { + // Parse the raw literal fields + if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + let name = content.parse::()?; + let ident = name.clone(); + + content.parse::()?; + + if content.peek(LitStr) { + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::CustomAttrText { name, value }, + }); + } else { + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::CustomAttrExpression { name, value }, + }); + } + + if content.is_empty() { + break; + } + + // todo: add a message saying you need to include commas between fields + if content.parse::().is_err() { + proc_macro_error::emit_error!( + ident, + "This attribute is misisng a trailing comma" + ) + } + continue; + } + if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { let name = content.parse::()?; + let ident = name.clone(); + let name_str = name.to_string(); content.parse::()?; if name_str.starts_with("on") { - if content.peek(token::Brace) { - let mycontent; - syn::braced!(mycontent in content); - - listeners.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: mycontent.parse()?, - }, - }); - } else { - listeners.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventClosure { - name, - closure: content.parse()?, - }, - }); - }; + listeners.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); } else { match name_str.as_str() { "key" => { key = Some(content.parse()?); } - "classes" => { - todo!("custom class list not supported") - } - "namespace" => { - todo!("custom namespace not supported") - } + "classes" => todo!("custom class list not supported yet"), + // "namespace" => todo!("custom namespace not supported yet"), "node_ref" => { _el_ref = Some(content.parse::()?); } @@ -96,27 +120,56 @@ impl Parse for Element { } } } - } else if content.peek(LitStr) && content.peek2(Token![:]) { - let name = content.parse::()?; - content.parse::()?; - if content.peek(LitStr) { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); - } else { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); + if content.is_empty() { + break; } - } else { - children.push(content.parse::()?); + + // todo: add a message saying you need to include commas between fields + if content.parse::().is_err() { + proc_macro_error::emit_error!( + ident, + "This attribute is misisng a trailing comma" + ) + } + continue; } + break; + } + + while !content.is_empty() { + if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { + let ident = content.parse::().unwrap(); + let name = ident.value(); + proc_macro_error::emit_error!( + ident, "This attribute `{}` is in the wrong place.", name; + help = +"All attribute fields must be placed above children elements. + + div { + attr: \"...\", <---- attribute is above children + div { } <---- children are below attributes + }"; + ) + } + + if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) { + let ident = content.parse::().unwrap(); + let name = ident.to_string(); + proc_macro_error::emit_error!( + ident, "This attribute `{}` is in the wrong place.", name; + help = +"All attribute fields must be placed above children elements. + + div { + attr: \"...\", <---- attribute is above children + div { } <---- children are below attributes + }"; + ) + } + + children.push(content.parse::()?); // consume comma if it exists // we don't actually care if there *are* commas after elements/text if content.peek(Token![,]) { @@ -138,7 +191,7 @@ impl Parse for Element { impl ToTokens for Element { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; - let childs = &self.children; + let children = &self.children; let listeners = &self.listeners; let attr = &self.attributes; @@ -153,7 +206,7 @@ impl ToTokens for Element { dioxus_elements::#name, [ #(#listeners),* ], [ #(#attr),* ], - [ #(#childs),* ], + [ #(#children),* ], #key, ) }); @@ -173,8 +226,8 @@ enum ElementAttr { // "attribute": true, CustomAttrExpression { name: LitStr, value: Expr }, - // onclick: move |_| {} - EventClosure { name: Ident, closure: ExprClosure }, + // // onclick: move |_| {} + // EventClosure { name: Ident, closure: ExprClosure }, // onclick: {} EventTokens { name: Ident, tokens: Expr }, @@ -189,7 +242,7 @@ impl ToTokens for ElementAttrNamed { fn to_tokens(&self, tokens: &mut TokenStream2) { let ElementAttrNamed { el_name, attr } = self; - let toks = match attr { + tokens.append_all(match attr { ElementAttr::AttrText { name, value } => { quote! { dioxus_elements::#el_name.#name(__cx, format_args_f!(#value)) @@ -200,26 +253,26 @@ impl ToTokens for ElementAttrNamed { dioxus_elements::#el_name.#name(__cx, #value) } } - ElementAttr::CustomAttrText { name, value } => { - quote! { __cx.attr( #name, format_args_f!(#value), None, false ) } - } - ElementAttr::CustomAttrExpression { name, value } => { - quote! { __cx.attr( #name, format_args_f!(#value), None, false ) } - } - - ElementAttr::EventClosure { name, closure } => { quote! { - dioxus_elements::on::#name(__cx, #closure) + __cx.attr( #name, format_args_f!(#value), None, false ) } } + ElementAttr::CustomAttrExpression { name, value } => { + quote! { + __cx.attr( #name, format_args_f!(#value), None, false ) + } + } + // ElementAttr::EventClosure { name, closure } => { + // quote! { + // dioxus_elements::on::#name(__cx, #closure) + // } + // } ElementAttr::EventTokens { name, tokens } => { quote! { dioxus_elements::on::#name(__cx, #tokens) } } - }; - - tokens.append_all(toks); + }); } } diff --git a/packages/core-macro/src/rsx/fragment.rs b/packages/core-macro/src/rsx/fragment.rs deleted file mode 100644 index 9743b1501..000000000 --- a/packages/core-macro/src/rsx/fragment.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Parse `Fragments` into the Fragment VNode -//! ========================================== -//! -//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the Fragment format. -//! We can be reasonably sure that whatever enters this parsing path is in the right format. -//! This feature must support: -//! - [x] Optional commas -//! - [ ] Children -//! - [ ] Keys - -use super::AmbiguousElement; -use syn::parse::ParseBuffer; -use { - proc_macro2::TokenStream as TokenStream2, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - parse::{Parse, ParseStream}, - Ident, Result, Token, - }, -}; - -pub struct Fragment { - children: Vec, -} - -impl Parse for Fragment { - fn parse(input: ParseStream) -> Result { - input.parse::()?; - - let children = Vec::new(); - - // parse the guts - let content: ParseBuffer; - syn::braced!(content in input); - while !content.is_empty() { - content.parse::()?; - - if content.peek(Token![,]) { - let _ = content.parse::(); - } - } - Ok(Self { children }) - } -} - -impl ToTokens for Fragment { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let childs = &self.children; - let children = quote! { - ChildrenList::new(__cx) - #( .add_child(#childs) )* - .finish() - }; - tokens.append_all(quote! { - // #key_token, - dioxus::builder::vfragment( - __cx, - None, - #children - ) - }) - } -} diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs index 5a0592b36..5ff5d85f7 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/core-macro/src/rsx/mod.rs @@ -11,17 +11,85 @@ //! //! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. -mod ambiguous; -mod body; mod component; mod element; -mod fragment; mod node; // Re-export the namespaces into each other -pub use ambiguous::*; -pub use body::*; pub use component::*; pub use element::*; -pub use fragment::*; pub use node::*; + +// imports +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + Ident, Result, Token, +}; + +pub struct CallBody { + custom_context: Option, + roots: Vec, +} + +impl Parse for CallBody { + fn parse(input: ParseStream) -> Result { + let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { + let name = input.parse::()?; + input.parse::()?; + + Some(name) + } else { + None + }; + + let mut roots = Vec::new(); + + while !input.is_empty() { + let node = input.parse::()?; + + if input.peek(Token![,]) { + let _ = input.parse::(); + } + + roots.push(node); + } + + Ok(Self { + custom_context, + roots, + }) + } +} + +/// Serialize the same way, regardless of flavor +impl ToTokens for CallBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let inner = if self.roots.len() == 1 { + let inner = &self.roots[0]; + quote! { #inner } + } else { + let childs = &self.roots; + quote! { __cx.fragment_root([ #(#childs),* ]) } + }; + + match &self.custom_context { + // The `in cx` pattern allows directly rendering + Some(ident) => out_tokens.append_all(quote! { + #ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + })) + }), + + // Otherwise we just build the LazyNode wrapper + None => out_tokens.append_all(quote! { + LazyNodes::new_some(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + }) + }), + }; + } +} diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs index e027711e6..d5d701767 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/core-macro/src/rsx/node.rs @@ -4,34 +4,67 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, + token, Expr, LitStr, Result, Token, }; -// ============================================== -// Parse any div {} as a VElement -// ============================================== +/* +Parse +-> div {} +-> Component {} +-> component() +-> "text {with_args}" +-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr +*/ pub enum BodyNode { - Element(AmbiguousElement), - Text(TextNode), + Element(Element), + Component(Component), + Text(LitStr), RawExpr(Expr), } impl Parse for BodyNode { fn parse(stream: ParseStream) -> Result { - // Supposedly this approach is discouraged due to inability to return proper errors - // TODO: Rework this to provide more informative errors - - if stream.peek(token::Brace) { - let content; - syn::braced!(content in stream); - return Ok(BodyNode::RawExpr(content.parse::()?)); - } - if stream.peek(LitStr) { - return Ok(BodyNode::Text(stream.parse::()?)); + return Ok(BodyNode::Text(stream.parse()?)); } - Ok(BodyNode::Element(stream.parse::()?)) + // div {} -> el + // Div {} -> comp + if stream.peek(syn::Ident) && stream.peek2(token::Brace) { + if stream + .fork() + .parse::()? + .to_string() + .chars() + .next() + .unwrap() + .is_ascii_uppercase() + { + return Ok(BodyNode::Component(stream.parse()?)); + } else { + return Ok(BodyNode::Element(stream.parse::()?)); + } + } + + // component() -> comp + // ::component {} -> comp + // ::component () -> comp + if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) + || (stream.peek(Token![::])) + || (stream.peek(Token![:]) && stream.peek2(Token![:])) + { + return Ok(BodyNode::Component(stream.parse::()?)); + } + + // crate::component{} -> comp + // crate::component() -> comp + if let Ok(pat) = stream.fork().parse::() { + if pat.segments.len() > 1 { + return Ok(BodyNode::Component(stream.parse::()?)); + } + } + + Ok(BodyNode::RawExpr(stream.parse::()?)) } } @@ -39,31 +72,13 @@ impl ToTokens for BodyNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match &self { BodyNode::Element(el) => el.to_tokens(tokens), - BodyNode::Text(txt) => txt.to_tokens(tokens), + BodyNode::Component(comp) => comp.to_tokens(tokens), + BodyNode::Text(txt) => tokens.append_all(quote! { + __cx.text(format_args_f!(#txt)) + }), BodyNode::RawExpr(exp) => tokens.append_all(quote! { __cx.fragment_from_iter(#exp) }), } } } - -// ======================================= -// Parse just plain text -// ======================================= -pub struct TextNode(LitStr); - -impl Parse for TextNode { - fn parse(s: ParseStream) -> Result { - Ok(Self(s.parse()?)) - } -} - -impl ToTokens for TextNode { - fn to_tokens(&self, tokens: &mut TokenStream2) { - // todo: use heuristics to see if we can promote to &static str - let token_stream = &self.0.to_token_stream(); - tokens.append_all(quote! { - __cx.text(format_args_f!(#token_stream)) - }); - } -} diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 2ebd1e250..36cca7f0c 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -734,9 +734,9 @@ impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> { } } -impl IntoVNode<'_> for &'static str { +impl<'b> IntoVNode<'_> for &'b str { fn into_vnode(self, cx: NodeFactory) -> VNode { - cx.static_text(self) + cx.text(format_args!("{}", self)) } } diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 21619d9c2..5d1dbcdf8 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -44,7 +44,7 @@ fn lists_work() { static App: Component = |cx| { cx.render(rsx!( h1 {"hello"} - {(0..6).map(|f| rsx!(span{ "{f}" }))} + (0..6).map(|f| rsx!(span{ "{f}" })) )) }; let mut vdom = VirtualDom::new(App); diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 6ef67efcb..9a02d6bcb 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -215,3 +215,67 @@ impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState<'a, !*self.get() } } + +/* + +Convenience methods for UseState. + +Note! + +This is not comprehensive. +This is *just* meant to make common operations easier. +*/ + +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; + +impl<'a, T: Copy + Add> Add for UseState<'a, T> { + type Output = T; + + fn add(self, rhs: T) -> Self::Output { + self.inner.current_val.add(rhs) + } +} +impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { + fn add_assign(&mut self, rhs: T) { + self.set(self.inner.current_val.add(rhs)); + } +} +impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { + type Output = T; + + fn sub(self, rhs: T) -> Self::Output { + self.inner.current_val.sub(rhs) + } +} +impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { + fn sub_assign(&mut self, rhs: T) { + self.set(self.inner.current_val.sub(rhs)); + } +} + +/// MUL +impl<'a, T: Copy + Mul> Mul for UseState<'a, T> { + type Output = T; + + fn mul(self, rhs: T) -> Self::Output { + self.inner.current_val.mul(rhs) + } +} +impl<'a, T: Copy + Mul> MulAssign for UseState<'a, T> { + fn mul_assign(&mut self, rhs: T) { + self.set(self.inner.current_val.mul(rhs)); + } +} +/// DIV +impl<'a, T: Copy + Div> Div for UseState<'a, T> { + type Output = T; + + fn div(self, rhs: T) -> Self::Output { + self.inner.current_val.div(rhs) + } +} +impl<'a, T: Copy + Div> DivAssign for UseState<'a, T> { + fn div_assign(&mut self, rhs: T) { + self.set(self.inner.current_val.div(rhs)); + } +} diff --git a/packages/router/src/link.rs b/packages/router/src/link.rs index 818d2d283..dd8e683d9 100644 --- a/packages/router/src/link.rs +++ b/packages/router/src/link.rs @@ -41,8 +41,8 @@ pub fn Link<'a, R: Routable>(cx: Scope<'a, LinkProps<'a, R>>) -> Element { a { href: "#", class: format_args!("{}", cx.props.class.unwrap_or("")), - {&cx.props.children} onclick: move |_| service.push_route(cx.props.to.clone()), + {&cx.props.children} } }) } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 23e93084f..aefd2062e 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -309,19 +309,18 @@ mod tests { static SLIGHTLY_MORE_COMPLEX: Component = |cx| { cx.render(rsx! { - div { - title: "About W3Schools" - {(0..20).map(|f| rsx!{ + div { title: "About W3Schools", + (0..20).map(|f| rsx!{ div { - title: "About W3Schools" - style: "color:blue;text-align:center" - class: "About W3Schools" + title: "About W3Schools", + style: "color:blue;text-align:center", + class: "About W3Schools", p { - title: "About W3Schools" + title: "About W3Schools", "Hello world!: {f}" } } - })} + }) } }) };