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}" } } - })} + }) } }) };