From d9b84f9f8f1588ef5d2cb5af4415ab9ccf457a60 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 19:33:34 -0800 Subject: [PATCH 01/11] Disambiguate expressions in rsx by requiring curlies --- examples/all_events.rs | 6 +- examples/calculator.rs | 8 +- examples/compose.rs | 4 +- examples/control_focus.rs | 2 +- examples/crm.rs | 4 +- examples/drops.rs | 4 +- examples/fermi.rs | 6 +- examples/file_explorer.rs | 20 +- examples/framework_benchmark.rs | 11 +- examples/inputs.rs | 6 +- .../openid_connect_demo/src/views/header.rs | 10 +- .../src/views/not_found.rs | 7 +- examples/pattern_model.rs | 10 +- examples/pattern_reducer.rs | 2 +- examples/query_segments_demo/src/main.rs | 4 +- examples/rsx_compile_fail.rs | 16 +- examples/rsx_usage.rs | 33 ++-- examples/scroll_to_top.rs | 2 +- examples/signals.rs | 6 +- examples/simple_list.rs | 10 +- examples/svg.rs | 2 +- examples/todomvc.rs | 68 ++++--- packages/autofmt/src/writer.rs | 9 +- packages/core/tests/create_dom.rs | 8 +- packages/core/tests/create_fragments.rs | 18 +- packages/core/tests/create_lists.rs | 4 +- packages/core/tests/create_passthru.rs | 4 +- packages/core/tests/diff_component.rs | 2 +- packages/core/tests/diff_keyed_list.rs | 24 +-- packages/core/tests/diff_unkeyed_list.rs | 28 +-- packages/core/tests/event_propagation.rs | 4 +- packages/core/tests/lifecycle.rs | 2 +- packages/core/tests/miri_full_app.rs | 4 +- packages/core/tests/miri_stress.rs | 4 +- packages/core/tests/task.rs | 2 +- packages/desktop/headless_tests/rendering.rs | 2 +- .../examples/all_terminal_events.rs | 2 +- packages/dioxus-tui/examples/buttons.rs | 56 +++--- packages/dioxus-tui/examples/color_test.rs | 37 ++-- packages/dioxus-tui/examples/list.rs | 4 +- .../examples/many_small_edit_stress.rs | 40 ++-- packages/dioxus/examples/stress.rs | 10 +- .../src/utils/persistant_iterator.rs | 37 ++-- packages/router/examples/simple_routes.rs | 2 +- .../router/src/components/history_buttons.rs | 4 +- packages/router/src/components/link.rs | 4 +- packages/rsx/src/node.rs | 185 +++++++++++------- packages/ssr/src/renderer.rs | 4 +- packages/ssr/tests/hydration.rs | 10 +- packages/ssr/tests/simple.rs | 12 +- packages/web/examples/hydrate.rs | 4 +- packages/web/examples/timeout_count.rs | 7 +- packages/web/tests/hydrate.rs | 2 +- 53 files changed, 401 insertions(+), 375 deletions(-) diff --git a/examples/all_events.rs b/examples/all_events.rs index 3f57ec7478..14fd1282e6 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -76,7 +76,11 @@ fn app(cx: Scope) -> Element { "Hover, click, type or scroll to see the info down below" } - div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) } + div { + for event in events.read().iter() { + div { "{event:?}" } + } + } } )) } diff --git a/examples/calculator.rs b/examples/calculator.rs index 2f8d4ee5f1..11151dcbf6 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -58,13 +58,13 @@ fn app(cx: Scope) -> Element { }; cx.render(rsx!( - style { include_str!("./assets/calculator.css") } + style { {include_str!("./assets/calculator.css")} } div { id: "wrapper", div { class: "app", div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event, - div { class: "calculator-display", val.to_string() } + div { class: "calculator-display", "{val}" } div { class: "calculator-keypad", div { class: "input-keys", div { class: "function-keys", @@ -103,14 +103,14 @@ fn app(cx: Scope) -> Element { div { class: "digit-keys", button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'), "●" } - (1..10).map(|k| rsx!{ + for k in 1..10 { button { class: "calculator-key {k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" } - }), + } } } div { class: "operator-keys", diff --git a/examples/compose.rs b/examples/compose.rs index 1331922814..180cd6983b 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -32,12 +32,12 @@ fn app(cx: Scope) -> Element { } ul { - emails_sent.read().iter().map(|message| cx.render(rsx! { + for message in emails_sent.read().iter() { li { h3 { "email" } span {"{message}"} } - })) + } } } }) diff --git a/examples/control_focus.rs b/examples/control_focus.rs index 4f4c2ddcb7..ac4e204181 100644 --- a/examples/control_focus.rs +++ b/examples/control_focus.rs @@ -16,7 +16,7 @@ fn app(cx: Scope) -> Element { loop { tokio::time::sleep(std::time::Duration::from_millis(10)).await; if let Some(element) = elements.read().get(focused) { - element.set_focus(true); + _ = element.set_focus(true).await; } else { focused = 0; } diff --git a/examples/crm.rs b/examples/crm.rs index 8af49b4017..cd9ea330dd 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -62,7 +62,7 @@ fn ClientList(cx: Scope) -> Element { Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" } Link { to: Route::Settings {}, class: "pure-button", "Settings" } - clients.read().iter().map(|client| rsx! { + for client in clients.read().iter() { div { class: "client", style: "margin-bottom: 50px", @@ -70,7 +70,7 @@ fn ClientList(cx: Scope) -> Element { p { "Name: {client.first_name} {client.last_name}" } p { "Description: {client.description}" } } - }) + } }) } diff --git a/examples/drops.rs b/examples/drops.rs index 71a7919907..956545e350 100644 --- a/examples/drops.rs +++ b/examples/drops.rs @@ -14,9 +14,9 @@ fn app(cx: Scope) -> Element { } render! { - (0..count).map(|_| rsx!{ + {(0..count).map(|_| rsx!{ drop_child {} - }) + })} } } diff --git a/examples/fermi.rs b/examples/fermi.rs index a15fc807f4..3250471a41 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -39,9 +39,9 @@ fn ChildWithRef(cx: Scope) -> Element { cx.render(rsx! { div { ul { - names.read().iter().map(|f| rsx!{ - li { "hello: {f}" } - }) + for name in names.read().iter() { + li { "hello: {name}" } + } } button { onclick: move |_| { diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 3ff757c60b..a99ac3232a 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -28,12 +28,12 @@ fn app(cx: Scope) -> Element { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } header { i { class: "material-icons icon-menu", "menu" } - h1 { "Files: ", files.read().current() } + h1 { "Files: ", {files.read().current()} } span { } i { class: "material-icons", onclick: move |_| files.write().go_up(), "logout" } } main { - files.read().path_names.iter().enumerate().map(|(dir_id, path)| { + {files.read().path_names.iter().enumerate().map(|(dir_id, path)| { let path_end = path.split('/').last().unwrap_or(path.as_str()); let icon_type = if path_end.contains('.') { "description" @@ -52,15 +52,13 @@ fn app(cx: Scope) -> Element { h1 { "{path_end}" } } ) - }), - files.read().err.as_ref().map(|err| { - rsx! ( - div { - code { "{err}" } - button { onclick: move |_| files.write().clear_err(), "x" } - } - ) - }) + })}, + if let Some(err) = files.read().err.as_ref() { + div { + code { "{err}" } + button { onclick: move |_| files.write().clear_err(), "x" } + } + } } } }) diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 7e7a320a91..3e286332ba 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -66,9 +66,9 @@ fn app(cx: Scope) -> Element { } table { 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}", + for (id, item) in items.read().iter().enumerate() { + tr { + class: if (*selected).map(|s| s == id).unwrap_or(false) { "danger" }, td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), @@ -80,8 +80,9 @@ fn app(cx: Scope) -> Element { } } td { class: "col-md-6" } - }) - }) + } + } + } } span { class: "preloadicon glyphicon glyphicon-remove", aria_hidden: "true" } diff --git a/examples/inputs.rs b/examples/inputs.rs index 311afc1f0f..00fc279eda 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -37,7 +37,7 @@ const FIELDS: &[(&str, &str)] = &[ fn app(cx: Scope) -> Element { cx.render(rsx! { div { margin_left: "30px", - select_example(cx), + {select_example(cx)}, div { // handling inputs on divs will catch all input events below // so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe) @@ -114,7 +114,7 @@ fn app(cx: Scope) -> Element { } } - FIELDS.iter().map(|(field, value)| rsx! { + for (field, value) in FIELDS.iter() { div { input { id: "{field}", @@ -131,7 +131,7 @@ fn app(cx: Scope) -> Element { } br {} } - }) + } } }) } diff --git a/examples/openid_connect_demo/src/views/header.rs b/examples/openid_connect_demo/src/views/header.rs index 1f2c6e0573..b9b99e584d 100644 --- a/examples/openid_connect_demo/src/views/header.rs +++ b/examples/openid_connect_demo/src/views/header.rs @@ -41,7 +41,7 @@ pub fn LogOut(cx: Scope) -> Element { } Err(error) => { rsx! { - div { format!{"Failed to load disconnection url: {:?}", error} } + div { "Failed to load disconnection url: {error:?}" } } } }, @@ -143,9 +143,9 @@ pub fn LoadClient(cx: Scope) -> Element { } } Err(error) => { + log::info! {"Failed to load client: {:?}", error}; rsx! { - div { format!{"Failed to load client: {:?}", error} } - log::info!{"Failed to load client: {:?}", error}, + div { "Failed to load client: {error:?}" } Outlet:: {} } } @@ -184,7 +184,7 @@ pub fn AuthHeader(cx: Scope) -> Element { Ok(email) => { rsx! { div { - div { email } + div { {email} } LogOut { client_id: client_props.client_id, client: client_props.client } Outlet:: {} } @@ -207,7 +207,7 @@ pub fn AuthHeader(cx: Scope) -> Element { log::info!("Other issue with token"); rsx! { div { - div { error.to_string() } + div { "{error}" } Outlet:: {} } } diff --git a/examples/openid_connect_demo/src/views/not_found.rs b/examples/openid_connect_demo/src/views/not_found.rs index 6321dc9fcb..b8fb1d8b8c 100644 --- a/examples/openid_connect_demo/src/views/not_found.rs +++ b/examples/openid_connect_demo/src/views/not_found.rs @@ -2,6 +2,9 @@ use dioxus::prelude::*; #[component] pub fn NotFound(cx: Scope, route: Vec) -> Element { - let routes = route.join(""); - render! {rsx! {div{routes}}} + render! { + div{ + {route.join("")} + } + } } diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index 2f6cc28035..13b5409e10 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -39,11 +39,11 @@ fn app(cx: Scope) -> Element { let state = use_ref(cx, Calculator::new); cx.render(rsx! { - style { include_str!("./assets/calculator.css") } + style { {include_str!("./assets/calculator.css")} } div { id: "wrapper", div { class: "app", div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt), - div { class: "calculator-display", state.read().formatted_display() } + div { class: "calculator-display", {state.read().formatted_display()} } div { class: "calculator-keypad", div { class: "input-keys", div { class: "function-keys", @@ -74,14 +74,14 @@ fn app(cx: Scope) -> Element { onclick: move |_| state.write().input_dot(), "●" } - (1..10).map(move |k| rsx!{ + for k in 1..10 { CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| state.write().input_digit(k), "{k}" } - }) + } } } div { class: "operator-keys", @@ -130,7 +130,7 @@ fn CalculatorKey<'a>(cx: Scope<'a, CalculatorKeyProps<'a>>) -> Element { button { class: "calculator-key {cx.props.name}", onclick: move |e| cx.props.onclick.call(e), - &cx.props.children + {&cx.props.children} } }) } diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index 6f7e16f6ef..98c4fb2c60 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { cx.render(rsx!( div { h1 {"Select an option"} - h3 { "The radio is... ", state.is_playing(), "!" } + h3 { "The radio is... ", {state.is_playing()}, "!" } button { onclick: move |_| state.make_mut().reduce(PlayerAction::Pause), "Pause" } diff --git a/examples/query_segments_demo/src/main.rs b/examples/query_segments_demo/src/main.rs index 3303aeecdd..24896a4032 100644 --- a/examples/query_segments_demo/src/main.rs +++ b/examples/query_segments_demo/src/main.rs @@ -66,7 +66,7 @@ impl FromQuery for ManualBlogQuerySegments { fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element { render! { div{"This is your blogpost with a query segment:"} - div{format!("{:?}", query_params)} + div{ "{query_params:?}" } } } @@ -74,7 +74,7 @@ fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element { fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element { render! { div{"This is your blogpost with a query segment:"} - div{format!("name={}&surname={}", name, surname)} + div{ "name={name}&surname={surname}" } } } diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 40bad709b8..ad2577b081 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -36,17 +36,17 @@ fn example(cx: Scope) -> Element { div { id: "asd", "your neighborhood spiderman" - items.iter().cycle().take(5).map(|f| rsx!{ - div { "{f.a}" } - }) + for item in items.iter().cycle().take(5) { + div { "{item.a}" } + } - things_list.iter().map(|f| rsx!{ - div { "{f.a}" "{f.b}" } - }) + for thing in things_list.iter() { + div { "{thing.a}" "{thing.b}" } + } - mything_read.as_ref().map(|f| rsx! { + if let Some(f) = mything_read.as_ref() { div { "{f}" } - }) + } } } )) diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 317288d558..dc452af482 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -61,7 +61,7 @@ fn App(cx: Scope) -> Element { 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 } + h1 {"Formatting without interpolation " {formatting_tuple.0} "and" {formatting_tuple.1} } h2 { "Multiple" "Text" @@ -94,10 +94,10 @@ fn App(cx: Scope) -> Element { } // Expressions can be used in element position too: - rsx!(p { "More templating!" }), + {rsx!(p { "More templating!" })}, // Iterators - (0..10).map(|i| rsx!(li { "{i}" })), + {(0..10).map(|i| rsx!(li { "{i}" }))}, // Iterators within expressions { @@ -117,24 +117,25 @@ fn App(cx: Scope) -> Element { // 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 {}))}, // Alternatively, you can use the "if" syntax - but both branches must be resolve to Element if false { - rsx!(h1 {"Top text"}) + h1 {"Top text"} } else { - rsx!(h1 {"Bottom text"}) + h1 {"Bottom text"} } // Using optionals for diverging branches - if true { + // Note that since this is wrapped in curlies, it's interpreted as an expression + {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<()>, + {None as Option<()>}, // can also just use empty fragments Fragment {} @@ -169,13 +170,13 @@ fn App(cx: Scope) -> Element { // Can pass in props directly as an expression { - let props = TallerProps {a: "hello", children: cx.render(rsx!(()))}; + let props = TallerProps {a: "hello", children: None }; rsx!(Taller { ..props }) } // Spreading can also be overridden manually Taller { - ..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )}, + ..TallerProps { a: "ballin!", children: None }, a: "not ballin!" } @@ -204,16 +205,16 @@ fn App(cx: Scope) -> Element { // helper functions // Anything that implements IntoVnode can be dropped directly into Rsx - helper(cx, "hello world!") + {helper(cx, "hello world!")} // Strings can be supplied directly - String::from("Hello world!") + {String::from("Hello world!")} // So can format_args - format_args!("Hello {}!", "world") + {format_args!("Hello {}!", "world")} // Or we can shell out to a helper function - format_dollars(10, 50) + {format_dollars(10, 50)} } }) } @@ -269,7 +270,7 @@ pub struct TallerProps<'a> { #[component] pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element { cx.render(rsx! { - &cx.props.children + {&cx.props.children} }) } diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs index 4eb38a4511..4159fd67aa 100644 --- a/examples/scroll_to_top.rs +++ b/examples/scroll_to_top.rs @@ -23,7 +23,7 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { if let Some(header) = header_element.read().as_ref() { - header.scroll_to(ScrollBehavior::Smooth); + _ = header.scroll_to(ScrollBehavior::Smooth); } }, "Scroll to top" diff --git a/examples/signals.rs b/examples/signals.rs index 0faac0af4b..e72488443a 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element { // We can do boolean operations on the current signal value if count.value() > 5 { - rsx!{ h2 { "High five!" } } + h2 { "High five!" } } // We can cleanly map signals with iterators @@ -41,9 +41,9 @@ fn app(cx: Scope) -> Element { // We can also use the signal value as a slice if let [ref first, .., ref last] = saved_values.read().as_slice() { - rsx! { li { "First and last: {first}, {last}" } } + li { "First and last: {first}, {last}" } } else { - rsx! { "No saved values" } + "No saved values" } }) } diff --git a/examples/simple_list.rs b/examples/simple_list.rs index 84cd9db710..feb5aa3b48 100644 --- a/examples/simple_list.rs +++ b/examples/simple_list.rs @@ -8,17 +8,17 @@ fn app(cx: Scope) -> Element { cx.render(rsx!( div { // Use Map directly to lazily pull elements - (0..10).map(|f| rsx! { "{f}" }), + {(0..10).map(|f| rsx! { "{f}" })}, // Collect into an intermediate collection if necessary, and call into_iter - ["a", "b", "c", "d", "e", "f"] + {["a", "b", "c", "d", "e", "f"] .into_iter() .map(|f| rsx! { "{f}" }) .collect::>() - .into_iter(), + .into_iter()}, // Use optionals - Some(rsx! { "Some" }), + {Some(rsx! { "Some" })}, // use a for loop where the body itself is RSX for name in 0..10 { @@ -27,7 +27,7 @@ fn app(cx: Scope) -> Element { // Or even use an unterminated conditional if true { - rsx!{ "hello world!" } + "hello world!" } } )) diff --git a/examples/svg.rs b/examples/svg.rs index 329cbe6ae5..f7a28d0cbb 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -97,7 +97,7 @@ pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element { fill: "{fill}", } - dots + {dots} } }) } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index c891ae832f..f3cb59f092 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -52,30 +52,30 @@ pub fn app(cx: Scope<()>) -> Element { TodoHeader { todos: todos } section { class: "main", if !todos.is_empty() { - rsx! { - input { - id: "toggle-all", - class: "toggle-all", - r#type: "checkbox", - onchange: move |_| { - let check = active_todo_count != 0; - for (_, item) in todos.make_mut().iter_mut() { - item.checked = check; - } - }, - checked: if active_todo_count == 0 { "true" } else { "false" }, - } - label { r#for: "toggle-all" } + input { + id: "toggle-all", + class: "toggle-all", + r#type: "checkbox", + onchange: move |_| { + let check = active_todo_count != 0; + for (_, item) in todos.make_mut().iter_mut() { + item.checked = check; + } + }, + checked: if active_todo_count == 0 { "true" } else { "false" }, } + label { r#for: "toggle-all" } } ul { class: "todo-list", - filtered_todos.iter().map(|id| rsx!(TodoEntry { - key: "{id}", - id: *id, - todos: todos, - })) + for id in filtered_todos.iter() { + TodoEntry { + key: "{id}", + id: *id, + todos: todos, + } + } } - (!todos.is_empty()).then(|| rsx!( + if !todos.is_empty() { ListFooter { active_todo_count: active_todo_count, active_todo_text: active_todo_text, @@ -83,7 +83,7 @@ pub fn app(cx: Scope<()>) -> Element { todos: todos, filter: filter, } - )) + } } } PageFooter {} @@ -172,7 +172,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { prevent_default: "onclick" } } - is_editing.then(|| rsx!{ + if **is_editing { input { class: "edit", value: "{todo.contents}", @@ -186,7 +186,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { } }, } - }) + } } }) } @@ -220,29 +220,27 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element { } ul { class: "filters", for (state , state_text , url) in [ - (FilterState::All, "All", "#/"), - (FilterState::Active, "Active", "#/active"), - (FilterState::Completed, "Completed", "#/completed"), -] { + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), + ] { li { a { href: url, class: selected(state), onclick: move |_| cx.props.filter.set(state), prevent_default: "onclick", - state_text + {state_text} } } } } if cx.props.show_clear_completed { - cx.render(rsx! { - button { - class: "clear-completed", - onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked), - "Clear completed" - } - }) + button { + class: "clear-completed", + onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked), + "Clear completed" + } } } }) diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index d569cdb9f3..c7bf300d5e 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,11 +1,11 @@ -use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop}; +use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; use std::{ collections::{HashMap, VecDeque}, fmt::{Result, Write}, }; -use syn::{spanned::Spanned, Expr, ExprIf}; +use syn::{spanned::Spanned, Expr}; use crate::buffer::Buffer; use crate::ifmt_to_string; @@ -231,8 +231,9 @@ impl<'a> Writer<'a> { Ok(()) } - fn write_if_chain(&mut self, ifchain: &ExprIf) -> std::fmt::Result { - self.write_raw_expr(ifchain.span()) + fn write_if_chain(&mut self, ifchain: &IfChain) -> std::fmt::Result { + todo!() + // self.write_raw_expr(ifchain.span()) } } diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 5e5005d3fc..eb8b26b3cc 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -84,7 +84,7 @@ fn create() { fn create_list() { let mut dom = VirtualDom::new(|cx| { cx.render(rsx! { - (0..3).map(|f| rsx!( div { "hello" } )) + {(0..3).map(|f| rsx!( div { "hello" } ))} }) }); @@ -148,7 +148,7 @@ fn create_components() { fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { cx.render(rsx! { h1 {} - div { &cx.props.children } + div { {&cx.props.children} } p {} }) } @@ -163,10 +163,10 @@ fn anchors() { let mut dom = VirtualDom::new(|cx| { cx.render(rsx! { if true { - rsx!( div { "hello" } ) + div { "hello" } } if false { - rsx!( div { "goodbye" } ) + div { "goodbye" } } }) }); diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs index ee312f34b0..fff90464a6 100644 --- a/packages/core/tests/create_fragments.rs +++ b/packages/core/tests/create_fragments.rs @@ -7,7 +7,7 @@ use dioxus_core::ElementId; #[test] fn empty_fragment_creates_nothing() { fn app(cx: Scope) -> Element { - cx.render(rsx!(())) + cx.render(rsx!({ () })) } let mut vdom = VirtualDom::new(app); @@ -43,18 +43,18 @@ fn fragments_nested() { cx.render(rsx!( div { "hello" } div { "goodbye" } - rsx! { + {rsx! { div { "hello" } div { "goodbye" } - rsx! { + {rsx! { div { "hello" } div { "goodbye" } - rsx! { + {rsx! { div { "hello" } div { "goodbye" } - } - } - } + }} + }} + }} )) }); @@ -79,7 +79,7 @@ fn fragments_across_components() { let world = "world"; cx.render(rsx! { "hellO!" - world + {world} }) } @@ -94,7 +94,7 @@ fn list_fragments() { fn app(cx: Scope) -> Element { cx.render(rsx!( h1 {"hello"} - (0..6).map(|f| rsx!( span { "{f}" })) + {(0..6).map(|f| rsx!( span { "{f}" }))} )) } assert_eq!( diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index ec3e8ecf2e..9a237ae909 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -11,12 +11,12 @@ use dioxus_core::ElementId; fn app(cx: Scope) -> Element { cx.render(rsx! { div { - (0..3).map(|i| rsx! { + for i in 0..3 { div { h1 { "hello world! "} p { "{i}" } } - }) + } } }) } diff --git a/packages/core/tests/create_passthru.rs b/packages/core/tests/create_passthru.rs index 4311cf1b14..704e92cf0d 100644 --- a/packages/core/tests/create_passthru.rs +++ b/packages/core/tests/create_passthru.rs @@ -20,7 +20,7 @@ fn nested_passthru_creates() { #[component] fn PassThru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { - cx.render(rsx!(children)) + cx.render(rsx!({ children })) } let mut dom = VirtualDom::new(App); @@ -60,7 +60,7 @@ fn nested_passthru_creates_add() { #[component] fn ChildComp<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { children }) + cx.render(rsx! { {children} }) } let mut dom = VirtualDom::new(App); diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs index 4766ad3781..5388bf6c1d 100644 --- a/packages/core/tests/diff_component.rs +++ b/packages/core/tests/diff_component.rs @@ -41,7 +41,7 @@ fn component_swap() { cx.render(rsx! { h1 { "NavBar" - (0..3).map(|_| rsx!(nav_link {})) + {(0..3).map(|_| rsx!(nav_link {}))} } }) } diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index ed14c2d958..d6ad152070 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -17,7 +17,7 @@ fn keyed_diffing_out_of_order() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); { @@ -59,7 +59,7 @@ fn keyed_diffing_out_of_order_adds() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -85,7 +85,7 @@ fn keyed_diffing_out_of_order_adds_3() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -111,7 +111,7 @@ fn keyed_diffing_out_of_order_adds_4() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -137,7 +137,7 @@ fn keyed_diffing_out_of_order_adds_5() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -162,7 +162,7 @@ fn keyed_diffing_additions() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -187,7 +187,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -216,7 +216,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -250,7 +250,7 @@ fn controlled_keyed_diffing_out_of_order() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -284,7 +284,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -313,7 +313,7 @@ fn remove_list() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); @@ -338,7 +338,7 @@ fn no_common_keys() { _ => unreachable!(), }; - cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + cx.render(rsx!({ order.iter().map(|i| rsx!(div { key: "{i}" })) })) }); _ = dom.rebuild(); diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index b5be161ef0..22c86fb70c 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -9,9 +9,9 @@ fn list_creates_one_by_one() { cx.render(rsx! { div { - (0..gen).map(|i| rsx! { + for i in 0..gen { div { "{i}" } - }) + } } }) }); @@ -78,9 +78,9 @@ fn removes_one_by_one() { cx.render(rsx! { div { - (0..gen).map(|i| rsx! { + for i in 0..gen { div { "{i}" } - }) + } } }) }); @@ -153,10 +153,10 @@ fn list_shrink_multiroot() { let mut dom = VirtualDom::new(|cx| { cx.render(rsx! { div { - (0..cx.generation()).map(|i| rsx! { + for i in 0..cx.generation() { div { "{i}" } div { "{i}" } - }) + } } }) }); @@ -214,10 +214,10 @@ fn removes_one_by_one_multiroot() { cx.render(rsx! { div { - (0..gen).map(|i| rsx! { + {(0..gen).map(|i| rsx! { div { "{i}" } div { "{i}" } - }) + })} } }) }); @@ -276,9 +276,9 @@ fn removes_one_by_one_multiroot() { fn two_equal_fragments_are_equal_static() { let mut dom = VirtualDom::new(|cx| { cx.render(rsx! { - (0..5).map(|_| rsx! { + for _ in 0..5 { div { "hello" } - }) + } }) }); @@ -290,9 +290,9 @@ fn two_equal_fragments_are_equal_static() { fn two_equal_fragments_are_equal() { let mut dom = VirtualDom::new(|cx| { cx.render(rsx! { - (0..5).map(|i| rsx! { + for i in 0..5 { div { "hello {i}" } - }) + } }) }); @@ -311,7 +311,9 @@ fn remove_many() { }; cx.render(rsx! { - (0..num).map(|i| rsx! { div { "hello {i}" } }) + for i in 0..num { + div { "hello {i}" } + } }) }); diff --git a/packages/core/tests/event_propagation.rs b/packages/core/tests/event_propagation.rs index fb87bb16a0..8878f1c599 100644 --- a/packages/core/tests/event_propagation.rs +++ b/packages/core/tests/event_propagation.rs @@ -58,11 +58,11 @@ fn app(cx: Scope) -> Element { *CLICKS.lock().unwrap() += 1; }, - vec![ + {vec![ render! { problematic_child {} } - ].into_iter() + ].into_iter()} } } } diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 6db294653e..c03e99124f 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -51,7 +51,7 @@ fn events_generate() { "Click me!" } }), - _ => cx.render(rsx!(())), + _ => None, } }; diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs index c285db7d55..91cef719f6 100644 --- a/packages/core/tests/miri_full_app.rs +++ b/packages/core/tests/miri_full_app.rs @@ -39,9 +39,9 @@ fn App(cx: Scope) -> Element { } button { onclick: move |_| idx -= 1, "-" } ul { - (0..**idx).map(|i| rsx! { + {(0..**idx).map(|i| rsx! { ChildExample { i: i, onhover: onhover } - }) + })} } } }) diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index ff8d69788d..5ddc7d8179 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -15,7 +15,7 @@ fn test_memory_leak() { cx.spawn(async {}); if val == 2 || val == 4 { - return cx.render(rsx!(())); + return None; } let name = cx.use_hook(|| String::from("numbers: ")); @@ -73,7 +73,7 @@ fn memo_works_properly() { let val = cx.generation(); if val == 2 || val == 4 { - return cx.render(rsx!(())); + return None; } let name = cx.use_hook(|| String::from("asd")); diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index e1ff55c85b..256e98c212 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -25,7 +25,7 @@ async fn it_works() { }); }); - cx.render(rsx!(())) + None } let mut dom = VirtualDom::new(app); diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 50d7b84196..9fa080be8b 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -89,7 +89,7 @@ fn check_html_renders(cx: Scope) -> Element { h1 { "text" } - dyn_element + {dyn_element} } } } diff --git a/packages/dioxus-tui/examples/all_terminal_events.rs b/packages/dioxus-tui/examples/all_terminal_events.rs index 004578807d..f58c9710f2 100644 --- a/packages/dioxus-tui/examples/all_terminal_events.rs +++ b/packages/dioxus-tui/examples/all_terminal_events.rs @@ -71,7 +71,7 @@ fn app(cx: Scope) -> Element { "Hover, click, type or scroll to see the info down below" } - div { width: "80%", height: "50%", flex_direction: "column", events_rendered } + div { width: "80%", height: "50%", flex_direction: "column", {events_rendered} } } }) } diff --git a/packages/dioxus-tui/examples/buttons.rs b/packages/dioxus-tui/examples/buttons.rs index f40a88434a..b8643b5a89 100644 --- a/packages/dioxus-tui/examples/buttons.rs +++ b/packages/dioxus-tui/examples/buttons.rs @@ -27,26 +27,25 @@ fn Button(cx: Scope) -> Element { height: "100%", background_color: "{color}", tabindex: "{cx.props.layer}", - onkeydown: |e| { + onkeydown: move |e| { if let Code::Space = e.inner().code() { toggle.modify(|f| !f); } }, - onclick: |_| { + onclick: move |_| { toggle.modify(|f| !f); }, - onmouseenter: |_|{ + onmouseenter: move |_| { hovered.set(true); }, - onmouseleave: |_|{ + onmouseleave: move |_|{ hovered.set(false); }, justify_content: "center", align_items: "center", display: "flex", flex_direction: "column", - - p{"tabindex: {cx.props.layer}"} + p{ "tabindex: {cx.props.layer}" } } }) } @@ -58,37 +57,28 @@ fn app(cx: Scope) -> Element { flex_direction: "column", width: "100%", height: "100%", - - (1..8).map(|y| - rsx!{ - div{ - display: "flex", - flex_direction: "row", - width: "100%", - height: "100%", - (1..8).map(|x|{ - if (x + y) % 2 == 0{ - rsx!{ - div{ - width: "100%", - height: "100%", - background_color: "rgb(100, 100, 100)", - } - } + for y in 1..8 { + div { + display: "flex", + flex_direction: "row", + width: "100%", + height: "100%", + for x in 1..8 { + if (x + y) % 2 == 0 { + div { + width: "100%", + height: "100%", + background_color: "rgb(100, 100, 100)", } - else{ - let layer = (x + y) % 3; - rsx!{ - Button{ - color_offset: x * y, - layer: layer as u16, - } - } + } else { + Button { + color_offset: x * y, + layer: ((x + y) % 3) as u16, } - }) + } } } - ) + } } }) } diff --git a/packages/dioxus-tui/examples/color_test.rs b/packages/dioxus-tui/examples/color_test.rs index 872fec53e4..90cbcb7c86 100644 --- a/packages/dioxus-tui/examples/color_test.rs +++ b/packages/dioxus-tui/examples/color_test.rs @@ -14,32 +14,25 @@ fn app(cx: Scope) -> Element { width: "100%", height: "100%", flex_direction: "column", - (0..=steps).map(|x| - { - let hue = x as f32*360.0/steps as f32; - rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "row", - (0..=steps).map(|y| - { - let alpha = y as f32*100.0/steps as f32; - rsx! { - div { - left: "{x}px", - top: "{y}px", - width: "10%", - height: "100%", - background_color: "hsl({hue}, 100%, 50%, {alpha}%)", - } - } + for x in 0..=steps { + div { width: "100%", height: "100%", flex_direction: "row", + for y in 0..=steps { + { + let hue = x as f32*360.0/steps as f32; + let alpha = y as f32*100.0/steps as f32; + rsx! { + div { + left: "{x}px", + top: "{y}px", + width: "10%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", } - ) + } } } } - ) + } } }) } diff --git a/packages/dioxus-tui/examples/list.rs b/packages/dioxus-tui/examples/list.rs index 5f41de399d..4ecc05e851 100644 --- a/packages/dioxus-tui/examples/list.rs +++ b/packages/dioxus-tui/examples/list.rs @@ -19,9 +19,9 @@ fn app(cx: Scope) -> Element { ul { flex_direction: "column", padding_left: "3px", - (0..10).map(|i| rsx!( + for i in 0..10 { "> hello {i}" - )) + } } } }) diff --git a/packages/dioxus-tui/examples/many_small_edit_stress.rs b/packages/dioxus-tui/examples/many_small_edit_stress.rs index 5f9d9d76f7..b783dd3838 100644 --- a/packages/dioxus-tui/examples/many_small_edit_stress.rs +++ b/packages/dioxus-tui/examples/many_small_edit_stress.rs @@ -71,32 +71,28 @@ fn Grid(cx: Scope) -> Element { width: "100%", height: "100%", flex_direction: "column", - (0..size).map(|x| - { - rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "row", - (0..size).map(|y| - { - let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; - let key = format!("{}-{}", x, y); - rsx! { - Box { - x: x, - y: y, - alpha: 100.0, - hue: alpha, - key: "{key}", - } - } + for x in 0..size { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + for y in 0..size { + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + rsx! { + Box { + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", } - ) + } } } } - ) + } } } } diff --git a/packages/dioxus/examples/stress.rs b/packages/dioxus/examples/stress.rs index 248d851d29..536b13bd0d 100644 --- a/packages/dioxus/examples/stress.rs +++ b/packages/dioxus/examples/stress.rs @@ -17,10 +17,12 @@ fn app(cx: Scope) -> Element { cx.render(rsx! ( table { tbody { - (0..10_000_usize).map(|f| { - let label = Label::new(&mut rng); - rsx!( table_row { row_id: f, label: label } ) - }) + for f in 0..10_000_usize { + table_row { + row_id: f, + label: Label::new(&mut rng) + } + } } } )) diff --git a/packages/native-core/src/utils/persistant_iterator.rs b/packages/native-core/src/utils/persistant_iterator.rs index 2442af29f0..dd328e160d 100644 --- a/packages/native-core/src/utils/persistant_iterator.rs +++ b/packages/native-core/src/utils/persistant_iterator.rs @@ -306,15 +306,10 @@ fn persist_removes() { _ => unreachable!(), }; render!( - div{ - (0..children).map(|i|{ - rsx!{ - p{ - key: "{i}", - "{i}" - } - } - }) + div { + for i in 0..children { + p { key: "{i}", "{i}" } + } } ) } @@ -387,15 +382,10 @@ fn persist_instertions_before() { _ => unreachable!(), }; render!( - div{ - (0..children).map(|i|{ - rsx!{ - p{ - key: "{i}", - "{i}" - } - } - }) + div { + for i in 0..children { + p { key: "{i}", "{i}" } + } } ) } @@ -446,14 +436,9 @@ fn persist_instertions_after() { }; render!( div{ - (0..children).map(|i|{ - rsx!{ - p{ - key: "{i}", - "{i}" - } - } - }) + for i in 0..children { + p { key: "{i}", "{i}" } + } } ) } diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index 9317f0f366..e7dc2289ab 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -103,7 +103,7 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin fn Route2(cx: Scope, user_id: usize) -> Element { render! { pre { "Route2{{\n\tuser_id:{user_id}\n}}" } - (0..*user_id).map(|i| rsx!{ p { "{i}" } }), + {(0..*user_id).map(|i| rsx!{ p { "{i}" } })}, p { "Footer" } Link { to: Route::Route3 { diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs index ae11ad2766..39c7a63a46 100644 --- a/packages/router/src/components/history_buttons.rs +++ b/packages/router/src/components/history_buttons.rs @@ -77,7 +77,7 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { disabled: "{disabled}", prevent_default: "onclick", onclick: move |_| router.go_back(), - children + {children} } } } @@ -149,7 +149,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { disabled: "{disabled}", prevent_default: "onclick", onclick: move |_| router.go_forward(), - children + {children} } } } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 639d99ae77..b0e7a9f13e 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -167,7 +167,7 @@ impl Debug for LinkProps<'_> { /// new_tab: true, /// rel: "link_rel", /// to: Route::Index {}, -/// +/// /// "A fully configured link" /// } /// } @@ -250,7 +250,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { id: "{id}", rel: "{rel}", target: "{tag_target}", - children + {children} } } } diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index c716110828..a6f0aea060 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -6,7 +6,8 @@ use syn::{ braced, parse::{Parse, ParseStream}, spanned::Spanned, - token, Expr, ExprIf, LitStr, Pat, Result, + token::{self, Brace}, + Expr, ExprIf, LitStr, Pat, Result, }; /* @@ -15,14 +16,14 @@ Parse -> Component {} -> component() -> "text {with_args}" --> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr +-> {(0..10).map(|f| rsx!("asd"))} // <--- notice the curly braces */ #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum BodyNode { Element(Element), Component(Component), ForLoop(ForLoop), - IfChain(ExprIf), + IfChain(IfChain), Text(IfmtInput), RawExpr(Expr), } @@ -112,7 +113,27 @@ impl Parse for BodyNode { return Ok(BodyNode::IfChain(stream.parse()?)); } - Ok(BodyNode::RawExpr(stream.parse::()?)) + // Match statements are special but have no special arm syntax + // we could allow arm syntax if we wanted + // + // ``` + // match { + // val => div {} + // other_val => div {} + // } + // ``` + if stream.peek(Token![match]) { + return Ok(BodyNode::RawExpr(stream.parse::()?)); + } + + if stream.peek(token::Brace) { + return Ok(BodyNode::RawExpr(stream.parse::()?)); + } + + Err(syn::Error::new( + stream.span(), + "Expected a valid body node.\nExpressions must be wrapped in curly braces.", + )) } } @@ -151,71 +172,52 @@ impl ToTokens for BodyNode { }) } BodyNode::IfChain(chain) => { - if is_if_chain_terminated(chain) { - tokens.append_all(quote! { - { - let ___nodes = (#chain).into_dyn_node(__cx); - ___nodes - } - }); - } else { - let ExprIf { + let mut body = TokenStream2::new(); + let mut terminated = false; + + let mut elif = Some(chain); + + while let Some(chain) = elif { + let IfChain { + if_token, cond, then_branch, + else_if_branch, else_branch, - .. } = chain; - let mut body = TokenStream2::new(); - - body.append_all(quote! { - if #cond { - Some(#then_branch) - } - }); - - let mut elif = else_branch; - - while let Some((_, ref branch)) = elif { - match branch.as_ref() { - Expr::If(ref eelif) => { - let ExprIf { - cond, - then_branch, - else_branch, - .. - } = eelif; - - body.append_all(quote! { - else if #cond { - Some(#then_branch) - } - }); - - elif = else_branch; - } - _ => { - body.append_all(quote! { - else { - #branch - } - }); - break; - } - } + let mut renderer: TemplateRenderer = TemplateRenderer { + roots: then_branch, + location: None, + }; + + body.append_all(quote! { #if_token #cond { Some({#renderer}) } }); + + if let Some(next) = else_if_branch { + body.append_all(quote! { else }); + elif = Some(next); + } else if let Some(else_branch) = else_branch { + renderer.roots = else_branch; + body.append_all(quote! { else { Some({#renderer}) } }); + terminated = true; + break; + } else { + elif = None; } + } + if !terminated { body.append_all(quote! { else { None } }); - - tokens.append_all(quote! { - { - let ___nodes = (#body).into_dyn_node(__cx); - ___nodes - } - }); } + + tokens.append_all(quote! { + { + let ___nodes = (#body).into_dyn_node(__cx); + ___nodes + } + }); } } } @@ -240,26 +242,73 @@ impl Parse for ForLoop { let in_token: Token![in] = input.parse()?; let expr: Expr = input.call(Expr::parse_without_eager_brace)?; - let content; - let brace_token = braced!(content in input); - - let mut children = vec![]; - - while !content.is_empty() { - children.push(content.parse()?); - } + let (brace_token, body) = parse_buffer_as_braced_children(input)?; Ok(Self { for_token, pat, in_token, - body: children, - expr: Box::new(expr), + body, brace_token, + expr: Box::new(expr), + }) + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct IfChain { + pub if_token: Token![if], + pub cond: Box, + pub then_branch: Vec, + pub else_if_branch: Option>, + pub else_branch: Option>, +} + +impl Parse for IfChain { + fn parse(input: ParseStream) -> Result { + let if_token: Token![if] = input.parse()?; + + // stolen from ExprIf + let cond = Box::new(input.call(Expr::parse_without_eager_brace)?); + + let (_, then_branch) = parse_buffer_as_braced_children(input)?; + + let mut else_branch = None; + let mut else_if_branch = None; + + // if the next token is `else`, set the else branch as the next if chain + if input.peek(Token![else]) { + input.parse::()?; + if input.peek(Token![if]) { + else_if_branch = Some(Box::new(input.parse::()?)); + } else { + let (_, else_branch_nodes) = parse_buffer_as_braced_children(input)?; + else_branch = Some(else_branch_nodes); + } + } + + Ok(Self { + cond, + if_token, + then_branch, + else_if_branch, + else_branch, }) } } +fn parse_buffer_as_braced_children( + input: &syn::parse::ParseBuffer<'_>, +) -> Result<(Brace, Vec)> { + let content; + let brace_token = braced!(content in input); + let mut then_branch = vec![]; + while !content.is_empty() { + then_branch.push(content.parse()?); + } + Ok((brace_token, then_branch)) +} + pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool { let mut current = chain; loop { diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 65ab4d0ed3..6e3229de7d 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -242,7 +242,9 @@ fn to_string_works() { div {} div { "nest 2" } "{dyn2}" - (0..5).map(|i| rsx! { div { "finalize {i}" } }) + for i in (0..5) { + div { "finalize {i}" } + } } } } diff --git a/packages/ssr/tests/hydration.rs b/packages/ssr/tests/hydration.rs index 8c9e9f5c82..b8d1912bf3 100644 --- a/packages/ssr/tests/hydration.rs +++ b/packages/ssr/tests/hydration.rs @@ -70,7 +70,7 @@ fn text_nodes() { fn app(cx: Scope) -> Element { let dynamic_text = "hello"; render! { - div { dynamic_text } + div { {dynamic_text} } } } @@ -124,7 +124,7 @@ fn components_hydrate() { fn Child2(cx: Scope) -> Element { let dyn_text = "hello"; render! { - div { dyn_text } + div { {dyn_text} } } } @@ -159,11 +159,7 @@ fn components_hydrate() { fn Child4(cx: Scope) -> Element { render! { for _ in 0..2 { - render! { - render! { - "{1}" - } - } + {render! { "{1}" }} } } } diff --git a/packages/ssr/tests/simple.rs b/packages/ssr/tests/simple.rs index a9034df9bc..9e4a2e2b0b 100644 --- a/packages/ssr/tests/simple.rs +++ b/packages/ssr/tests/simple.rs @@ -23,9 +23,9 @@ fn lists() { assert_eq!( dioxus_ssr::render_lazy(rsx! { ul { - (0..5).map(|i| rsx! { + for i in 0..5 { li { "item {i}" } - }) + } } }), "
  • item 0
  • item 1
  • item 2
  • item 3
  • item 4
" @@ -53,9 +53,9 @@ fn components() { assert_eq!( dioxus_ssr::render_lazy(rsx! { div { - (0..5).map(|name| rsx! { + for name in 0..5 { MyComponent { name: name } - }) + } } }), "
component 0
component 1
component 2
component 3
component 4
" @@ -67,7 +67,9 @@ fn fragments() { assert_eq!( dioxus_ssr::render_lazy(rsx! { div { - (0..5).map(|_| rsx! (())) + for _ in 0..5 { + {()} + } } }), "
" diff --git a/packages/web/examples/hydrate.rs b/packages/web/examples/hydrate.rs index 8a6cd464ee..f11b430328 100644 --- a/packages/web/examples/hydrate.rs +++ b/packages/web/examples/hydrate.rs @@ -12,11 +12,11 @@ fn app(cx: Scope) -> Element { "asd" Bapp {} } - (0..10).map(|f| rsx!{ + {(0..10).map(|f| rsx!{ div { "thing {f}" } - }) + })} }) } diff --git a/packages/web/examples/timeout_count.rs b/packages/web/examples/timeout_count.rs index 742e6a49e7..5647e92d76 100644 --- a/packages/web/examples/timeout_count.rs +++ b/packages/web/examples/timeout_count.rs @@ -24,8 +24,11 @@ fn app(cx: Scope) -> Element { start(); *count.write() += 1; }, - // format is needed as {count} does not seemed to work in `if` within content - if **started { format!("Current score: {}", count.write()) } else { "Start".to_string() } + if **started { + "Current score: {count.read()}" + } else { + "Start" + } } }) } diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index c7de26a8c9..d86cd07da6 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -43,7 +43,7 @@ fn rehydrates() { }, "listener test" } - false.then(|| rsx!{ "hello" }) + {false.then(|| rsx!{ "hello" })} } }) } From fa9d92f956403e6428e13eff175c2df173f9ceee Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 22:41:40 -0800 Subject: [PATCH 02/11] Fix tests for autofmt --- examples/shorthand.rs | 25 ++ packages/autofmt/src/component.rs | 4 + packages/autofmt/src/writer.rs | 44 ++- .../autofmt/tests/samples/commentshard.rsx | 6 +- packages/autofmt/tests/samples/complex.rsx | 8 +- .../autofmt/tests/samples/ifchain_forloop.rsx | 16 +- .../autofmt/tests/samples/immediate_expr.rsx | 2 +- packages/autofmt/tests/samples/long.rsx | 8 +- packages/autofmt/tests/samples/long_exprs.rsx | 2 +- packages/autofmt/tests/samples/multirsx.rsx | 10 +- .../autofmt/tests/samples/trailing_expr.rsx | 2 +- .../autofmt/tests/wrong/multiexpr-4sp.rsx | 4 +- .../tests/wrong/multiexpr-4sp.wrong.rsx | 2 +- .../autofmt/tests/wrong/multiexpr-tab.rsx | 4 +- .../tests/wrong/multiexpr-tab.wrong.rsx | 2 +- packages/rsx/src/component.rs | 316 ++++++++++-------- packages/rsx/src/node.rs | 1 - 17 files changed, 283 insertions(+), 173 deletions(-) create mode 100644 examples/shorthand.rs diff --git a/examples/shorthand.rs b/examples/shorthand.rs new file mode 100644 index 0000000000..44a1650e6e --- /dev/null +++ b/examples/shorthand.rs @@ -0,0 +1,25 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let a = 123; + let b = 456; + let c = 789; + + render! { + Component { a, b, c } + Component { a, ..ComponentProps { a: 1, b: 2, c: 3 } } + } +} + +#[component] +fn Component(cx: Scope, a: i32, b: i32, c: i32) -> Element { + render! { + div { "{a}" } + div { "{b}" } + div { "{c}" } + } +} diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index 1eea2eba22..c83f9b3335 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -182,6 +182,9 @@ impl Writer<'_> { s.source.as_ref().unwrap().to_token_stream() )?; } + ContentField::Shorthand(e) => { + write!(self.out, "{}", e.to_token_stream())?; + } ContentField::OnHandlerRaw(exp) => { let out = prettyplease::unparse_expr(exp); let mut lines = out.split('\n').peekable(); @@ -223,6 +226,7 @@ impl Writer<'_> { .iter() .map(|field| match &field.content { ContentField::Formatted(s) => ifmt_to_string(s).len() , + ContentField::Shorthand(e) => e.to_token_stream().to_string().len(), ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => { let formatted = prettyplease::unparse_expr(exp); let len = if formatted.contains('\n') { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index c7bf300d5e..78736acfe6 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -232,8 +232,48 @@ impl<'a> Writer<'a> { } fn write_if_chain(&mut self, ifchain: &IfChain) -> std::fmt::Result { - todo!() - // self.write_raw_expr(ifchain.span()) + // Recurse in place by setting the next chain + let mut branch = Some(ifchain); + + while let Some(chain) = branch { + let IfChain { + if_token, + cond, + then_branch, + else_if_branch, + else_branch, + } = chain; + + write!( + self.out, + "{} {} {{", + if_token.to_token_stream(), + prettyplease::unparse_expr(cond) + )?; + + self.write_body_indented(then_branch)?; + + if let Some(else_if_branch) = else_if_branch { + // write the closing bracket and else + self.out.tabbed_line()?; + write!(self.out, "}} else ")?; + + branch = Some(else_if_branch); + } else if let Some(else_branch) = else_branch { + self.out.tabbed_line()?; + write!(self.out, "}} else {{")?; + + self.write_body_indented(else_branch)?; + branch = None; + } else { + branch = None; + } + } + + self.out.tabbed_line()?; + write!(self.out, "}}")?; + + Ok(()) } } diff --git a/packages/autofmt/tests/samples/commentshard.rsx b/packages/autofmt/tests/samples/commentshard.rsx index f6c2fc19e2..71782f39d3 100644 --- a/packages/autofmt/tests/samples/commentshard.rsx +++ b/packages/autofmt/tests/samples/commentshard.rsx @@ -8,17 +8,17 @@ rsx! { "hello world" // Comments - expr1, + {expr1}, // Comments - expr2, + {expr2}, // Comments // Comments // Comments // Comments // Comments - expr3, + {expr3}, div { // todo some work in here diff --git a/packages/autofmt/tests/samples/complex.rsx b/packages/autofmt/tests/samples/complex.rsx index 1bf29a7671..56c16e6577 100644 --- a/packages/autofmt/tests/samples/complex.rsx +++ b/packages/autofmt/tests/samples/complex.rsx @@ -18,7 +18,7 @@ rsx! { to: "{to}", span { class: "inline-block mr-3", icons::icon_0 {} } span { "{name}" } - children.is_some().then(|| rsx! { + {children.is_some().then(|| rsx! { span { class: "inline-block ml-auto hover:bg-gray-500", onclick: move |evt| { @@ -27,9 +27,9 @@ rsx! { }, icons::icon_8 {} } - }) + })} } - div { class: "px-4", is_current.then(|| rsx!{ children }) } + div { class: "px-4", {is_current.then(|| rsx!{ children })} } } // No nesting @@ -48,5 +48,5 @@ rsx! { } } - div { asdbascasdbasd, asbdasbdabsd, asbdabsdbasdbas } + div { "asdbascasdbasd", "asbdasbdabsd", {asbdabsdbasdbas} } } diff --git a/packages/autofmt/tests/samples/ifchain_forloop.rsx b/packages/autofmt/tests/samples/ifchain_forloop.rsx index 9ec7326afd..728b2d3d38 100644 --- a/packages/autofmt/tests/samples/ifchain_forloop.rsx +++ b/packages/autofmt/tests/samples/ifchain_forloop.rsx @@ -8,6 +8,20 @@ rsx! { // Some ifchain if a > 10 { // - rsx! { div {} } + div {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else if a > 20 { + h1 {} + } else { + h3 {} } } diff --git a/packages/autofmt/tests/samples/immediate_expr.rsx b/packages/autofmt/tests/samples/immediate_expr.rsx index 9e298499d5..cea8dc7dbf 100644 --- a/packages/autofmt/tests/samples/immediate_expr.rsx +++ b/packages/autofmt/tests/samples/immediate_expr.rsx @@ -1,4 +1,4 @@ fn it_works() { - cx.render(rsx!(())) + cx.render(rsx!({()})) } diff --git a/packages/autofmt/tests/samples/long.rsx b/packages/autofmt/tests/samples/long.rsx index 1bda8a006a..056e16061d 100644 --- a/packages/autofmt/tests/samples/long.rsx +++ b/packages/autofmt/tests/samples/long.rsx @@ -11,7 +11,7 @@ pub fn Explainer<'a>( // pt-5 sm:pt-24 lg:pt-24 let mut right = rsx! { - div { class: "relative w-1/2", flasher } + div { class: "relative w-1/2", {flasher} } }; let align = match invert { @@ -24,7 +24,7 @@ pub fn Explainer<'a>( h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold", "{title}" } - content + {content} } }; @@ -34,8 +34,8 @@ pub fn Explainer<'a>( cx.render(rsx! { div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", - left, - right + {left}, + {right} } }) } diff --git a/packages/autofmt/tests/samples/long_exprs.rsx b/packages/autofmt/tests/samples/long_exprs.rsx index fb4dab93fa..490bc55418 100644 --- a/packages/autofmt/tests/samples/long_exprs.rsx +++ b/packages/autofmt/tests/samples/long_exprs.rsx @@ -6,7 +6,7 @@ rsx! { section { class: "body-font overflow-hidden dark:bg-ideblack", div { class: "container px-6 mx-auto", div { class: "-my-8 divide-y-2 divide-gray-100", - POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } }) + {POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } })} } } } diff --git a/packages/autofmt/tests/samples/multirsx.rsx b/packages/autofmt/tests/samples/multirsx.rsx index 380ef34874..84340f5baa 100644 --- a/packages/autofmt/tests/samples/multirsx.rsx +++ b/packages/autofmt/tests/samples/multirsx.rsx @@ -4,20 +4,20 @@ rsx! { div {} // hi - div { abcd, ball, s } + div { "abcd", "ball", "s" } // // // - div { abcd, ball, s } + div { "abcd", "ball", "s" } // // // div { - abcd, - ball, - s, + "abcd" + "ball" + "s" // "asdasd" diff --git a/packages/autofmt/tests/samples/trailing_expr.rsx b/packages/autofmt/tests/samples/trailing_expr.rsx index 667b8bb326..a57a822ab7 100644 --- a/packages/autofmt/tests/samples/trailing_expr.rsx +++ b/packages/autofmt/tests/samples/trailing_expr.rsx @@ -1,7 +1,7 @@ fn it_works() { cx.render(rsx! { div { - span { "Description: ", package.description.as_deref().unwrap_or("❌❌❌❌ missing") } + span { "Description: ", {package.description.as_deref().unwrap_or("❌❌❌❌ missing")} } } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-4sp.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx index 56c57e37e1..ecb2f8bdc0 100644 --- a/packages/autofmt/tests/wrong/multiexpr-4sp.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx @@ -1,8 +1,8 @@ fn ItWroks() { cx.render(rsx! { div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", - left, - right + {left}, + {right} } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx index 5732f2cddf..15752f1723 100644 --- a/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx @@ -1,5 +1,5 @@ fn ItWroks() { cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", {left}, {right} } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.rsx index 1fc3401c48..85f2216a1e 100644 --- a/packages/autofmt/tests/wrong/multiexpr-tab.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-tab.rsx @@ -1,8 +1,8 @@ fn ItWroks() { cx.render(rsx! { div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", - left, - right + {left}, + {right} } }) } diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx index 0731075417..6872d0b432 100644 --- a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx +++ b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx @@ -1,5 +1,5 @@ fn ItWroks() { cx.render(rsx! { - div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", {left}, {right} } }) } diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index bccef2638c..528a39072a 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -19,6 +19,7 @@ use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, spanned::Spanned, + token::Brace, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result, Token, }; @@ -32,62 +33,15 @@ pub struct Component { pub brace: syn::token::Brace, } -impl Component { - pub fn validate_component_path(path: &syn::Path) -> Result<()> { - // ensure path segments doesn't have PathArguments, only the last - // segment is allowed to have one. - if path - .segments - .iter() - .take(path.segments.len() - 1) - .any(|seg| seg.arguments != PathArguments::None) - { - component_path_cannot_have_arguments!(path.span()); - } - - // ensure last segment only have value of None or AngleBracketed - if !matches!( - path.segments.last().unwrap().arguments, - PathArguments::None | PathArguments::AngleBracketed(_) - ) { - invalid_component_path!(path.span()); - } - - Ok(()) - } - - pub fn key(&self) -> Option<&IfmtInput> { - match self - .fields - .iter() - .find(|f| f.name == "key") - .map(|f| &f.content) - { - Some(ContentField::Formatted(fmt)) => Some(fmt), - _ => None, - } - } -} - impl Parse for Component { fn parse(stream: ParseStream) -> Result { let mut name = stream.parse::()?; Component::validate_component_path(&name)?; // extract the path arguments from the path into prop_gen_args - let prop_gen_args = name.segments.last_mut().and_then(|seg| { - if let PathArguments::AngleBracketed(args) = seg.arguments.clone() { - seg.arguments = PathArguments::None; - Some(args) - } else { - None - } - }); + let prop_gen_args = normalize_path(&mut name); let content: ParseBuffer; - - // if we see a `{` then we have a block - // else parse as a function-like call let brace = syn::braced!(content in stream); let mut fields = Vec::new(); @@ -98,11 +52,25 @@ impl Parse for Component { // 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![:]) { - fields.push(content.parse::()?); + manual_props = Some(content.parse()?); + } else + // + // Fields + // Named fields + if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + fields.push(content.parse()?); + } else + // + // shorthand struct initialization + // Not a div {}, mod::Component {}, or web-component {} + if content.peek(Ident) + && !content.peek2(Brace) + && !content.peek2(Token![:]) + && !content.peek2(Token![-]) + { + fields.push(content.parse()?); } else { - children.push(content.parse::()?); + children.push(content.parse()?); } if content.peek(Token![,]) { @@ -123,74 +91,23 @@ impl Parse for Component { impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - let prop_gen_args = &self.prop_gen_args; - - let builder = match &self.manual_props { - Some(manual_props) => { - let mut toks = quote! { - let mut __manual_props = #manual_props; - }; - for field in &self.fields { - if field.name == "key" { - // skip keys - } else { - let name = &field.name; - let val = &field.content; - toks.append_all(quote! { - __manual_props.#name = #val; - }); - } - } - toks.append_all(quote! { - __manual_props - }); - quote! {{ - #toks - }} - } - None => { - let mut toks = match prop_gen_args { - Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) }, - None => quote! { fc_to_builder(__cx, #name) }, - }; - for field in &self.fields { - match field.name.to_string().as_str() { - "key" => {} - _ => toks.append_all(quote! {#field}), - } - } - - if !self.children.is_empty() { - let renderer: TemplateRenderer = TemplateRenderer { - roots: &self.children, - location: None, - }; - - toks.append_all(quote! { - .children( - Some({ #renderer }) - ) - }); - } - - toks.append_all(quote! { - .build() - }); - toks - } - }; + let Self { + name, + prop_gen_args, + .. + } = self; - let fn_name = self.name.segments.last().unwrap().ident.to_string(); + let builder = self + .manual_props + .as_ref() + .map(|props| self.collect_manual_props(props)) + .unwrap_or_else(|| self.collect_props()); - let gen_name = match &self.prop_gen_args { - Some(gen) => quote! { #name #gen }, - None => quote! { #name }, - }; + let fn_name = self.fn_name(); tokens.append_all(quote! { __cx.component( - #gen_name, + #name #prop_gen_args, #builder, #fn_name ) @@ -198,6 +115,91 @@ impl ToTokens for Component { } } +impl Component { + fn validate_component_path(path: &syn::Path) -> Result<()> { + // ensure path segments doesn't have PathArguments, only the last + // segment is allowed to have one. + if path + .segments + .iter() + .take(path.segments.len() - 1) + .any(|seg| seg.arguments != PathArguments::None) + { + component_path_cannot_have_arguments!(path.span()); + } + + // ensure last segment only have value of None or AngleBracketed + if !matches!( + path.segments.last().unwrap().arguments, + PathArguments::None | PathArguments::AngleBracketed(_) + ) { + invalid_component_path!(path.span()); + } + + Ok(()) + } + + pub fn key(&self) -> Option<&IfmtInput> { + match self + .fields + .iter() + .find(|f| f.name == "key") + .map(|f| &f.content) + { + Some(ContentField::Formatted(fmt)) => Some(fmt), + _ => None, + } + } + + fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 { + let mut toks = quote! { let mut __manual_props = #manual_props; }; + for field in &self.fields { + if field.name == "key" { + continue; + } + let ComponentField { name, content } = field; + toks.append_all(quote! { __manual_props.#name = #content; }); + } + toks.append_all(quote! { __manual_props }); + quote! {{ #toks }} + } + + fn collect_props(&self) -> TokenStream2 { + let name = &self.name; + + let mut toks = match &self.prop_gen_args { + Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) }, + None => quote! { fc_to_builder(__cx, #name) }, + }; + for field in &self.fields { + match field.name.to_string().as_str() { + "key" => {} + _ => toks.append_all(quote! {#field}), + } + } + if !self.children.is_empty() { + let renderer: TemplateRenderer = TemplateRenderer { + roots: &self.children, + location: None, + }; + + toks.append_all(quote! { + .children( + Some({ #renderer }) + ) + }); + } + toks.append_all(quote! { + .build() + }); + toks + } + + fn fn_name(&self) -> String { + self.name.segments.last().unwrap().ident.to_string() + } +} + // the struct's fields info #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct ComponentField { @@ -207,21 +209,47 @@ pub struct ComponentField { #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum ContentField { + Shorthand(Ident), ManExpr(Expr), Formatted(IfmtInput), OnHandlerRaw(Expr), } +impl ContentField { + fn new_from_name(name: &Ident, input: ParseStream) -> Result { + if name.to_string().starts_with("on") { + return Ok(ContentField::OnHandlerRaw(input.parse()?)); + } + + if *name == "key" { + return Ok(ContentField::Formatted(input.parse()?)); + } + + if input.peek(LitStr) { + let forked = input.fork(); + let t: LitStr = forked.parse()?; + + // the string literal must either be the end of the input or a followed by a comma + let res = + match (forked.is_empty() || forked.peek(Token![,])) && is_literal_foramtted(&t) { + true => ContentField::Formatted(input.parse()?), + false => ContentField::ManExpr(input.parse()?), + }; + + return Ok(res); + } + + Ok(ContentField::ManExpr(input.parse()?)) + } +} + impl ToTokens for ContentField { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { + ContentField::Shorthand(i) => tokens.append_all(quote! { #i }), ContentField::ManExpr(e) => e.to_tokens(tokens), - ContentField::Formatted(s) => tokens.append_all(quote! { - __cx.raw_text(#s) - }), - ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { - __cx.event_handler(#e) - }), + ContentField::Formatted(s) => tokens.append_all(quote! { __cx.raw_text(#s) }), + ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { __cx.event_handler(#e) }), } } } @@ -229,30 +257,21 @@ impl ToTokens for ContentField { impl Parse for ComponentField { fn parse(input: ParseStream) -> Result { let name = Ident::parse_any(input)?; - input.parse::()?; - - let content = { - if name.to_string().starts_with("on") { - ContentField::OnHandlerRaw(input.parse()?) - } else if name == "key" { - let content = ContentField::Formatted(input.parse()?); - return Ok(Self { name, content }); - } else if input.peek(LitStr) { - let forked = input.fork(); - let t: LitStr = forked.parse()?; - // the string literal must either be the end of the input or a followed by a comma - if (forked.is_empty() || forked.peek(Token![,])) && is_literal_foramtted(&t) { - ContentField::Formatted(input.parse()?) - } else { - ContentField::ManExpr(input.parse()?) - } - } else { - ContentField::ManExpr(input.parse()?) - } + + // if the next token is not a colon, then it's a shorthand field + if input.parse::().is_err() { + return Ok(Self { + content: ContentField::Shorthand(name.clone()), + name, + }); }; + + let content = ContentField::new_from_name(&name, input)?; + if input.peek(LitStr) || input.peek(Ident) { missing_trailing_comma!(content.span()); } + Ok(Self { name, content }) } } @@ -260,9 +279,7 @@ impl Parse for ComponentField { impl ToTokens for ComponentField { fn to_tokens(&self, tokens: &mut TokenStream2) { let ComponentField { name, content, .. } = self; - tokens.append_all(quote! { - .#name(#content) - }) + tokens.append_all(quote! { .#name(#content) }) } } @@ -281,3 +298,14 @@ fn is_literal_foramtted(lit: &LitStr) -> bool { false } + +fn normalize_path(name: &mut syn::Path) -> Option { + let seg = name.segments.last_mut()?; + match seg.arguments.clone() { + PathArguments::AngleBracketed(args) => { + seg.arguments = PathArguments::None; + Some(args) + } + _ => None, + } +} diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index a6f0aea060..02fab0752a 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -98,7 +98,6 @@ impl Parse for BodyNode { // Input:: {} // crate::Input:: {} if body_stream.peek(token::Brace) { - Component::validate_component_path(&path)?; return Ok(BodyNode::Component(stream.parse()?)); } } From c5dfbd791331e3d597dcc65822876fb51975eeb4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 22:52:41 -0800 Subject: [PATCH 03/11] fix test --- examples/shorthand.rs | 3 +++ packages/cli/src/cli/autoformat.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/shorthand.rs b/examples/shorthand.rs index 44a1650e6e..4a3e9b4161 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -8,8 +8,11 @@ fn app(cx: Scope) -> Element { let a = 123; let b = 456; let c = 789; + let class = "class"; + let id = "id"; render! { + div { class, id } Component { a, b, c } Component { a, ..ComponentProps { a: 1, b: 2, c: 3 } } } diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 7909cdb88e..d1702ebf21 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -267,10 +267,10 @@ async fn test_auto_fmt() { let test_rsx = r#" // - rsx! { + div {} - } + // // From f1e8faffb59b0228716d52507f89c7cc390778a2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 23:17:01 -0800 Subject: [PATCH 04/11] fix tests --- packages/autofmt/src/element.rs | 3 ++ packages/autofmt/src/writer.rs | 1 + packages/core/tests/miri_stress.rs | 2 +- packages/rsx/src/attribute.rs | 17 +++++- packages/rsx/src/element.rs | 86 ++++++++++++++++++------------ packages/rsx/src/lib.rs | 12 ++--- 6 files changed, 77 insertions(+), 44 deletions(-) diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 1354666f38..d386f8b721 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -237,6 +237,9 @@ impl Writer<'_> { ElementAttrValue::AttrLiteral(value) => { write!(self.out, "{value}", value = ifmt_to_string(value))?; } + ElementAttrValue::Shorthand(value) => { + write!(self.out, "{value}",)?; + } ElementAttrValue::AttrExpr(value) => { let out = prettyplease::unparse_expr(value); let mut lines = out.split('\n').peekable(); diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index 78736acfe6..be375aa9d4 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -142,6 +142,7 @@ impl<'a> Writer<'a> { } ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), + ElementAttrValue::Shorthand(expr) => expr.span().line_length(), ElementAttrValue::EventTokens(tokens) => { let location = Location::new(tokens.span().start()); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 5ddc7d8179..f571b30ced 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -15,7 +15,7 @@ fn test_memory_leak() { cx.spawn(async {}); if val == 2 || val == 4 { - return None; + return render!({ () }); } let name = cx.use_hook(|| String::from("numbers: ")); diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 4c0db5a333..c5d3383b0b 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -20,6 +20,13 @@ impl AttributeType { } } + pub fn matches_attr_name(&self, other: &Self) -> bool { + match (self, other) { + (Self::Named(a), Self::Named(b)) => a.attr.name == b.attr.name, + _ => false, + } + } + pub(crate) fn try_combine(&self, other: &Self) -> Option { match (self, other) { (Self::Named(a), Self::Named(b)) => a.try_combine(b).map(Self::Named), @@ -105,6 +112,7 @@ impl ToTokens for ElementAttrNamed { match &attr.value { ElementAttrValue::AttrLiteral(_) | ElementAttrValue::AttrExpr(_) + | ElementAttrValue::Shorthand(_) | ElementAttrValue::AttrOptionalExpr { .. } => { let name = &self.attr.name; let ns = ns(name); @@ -144,6 +152,8 @@ pub struct ElementAttr { #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum ElementAttrValue { + /// attribute, + Shorthand(Ident), /// attribute: "value" AttrLiteral(IfmtInput), /// attribute: if bool { "value" } @@ -159,7 +169,7 @@ pub enum ElementAttrValue { impl Parse for ElementAttrValue { fn parse(input: ParseStream) -> syn::Result { - Ok(if input.peek(Token![if]) { + let element_attr_value = if input.peek(Token![if]) { let if_expr = input.parse::()?; if is_if_chain_terminated(&if_expr) { ElementAttrValue::AttrExpr(Expr::If(if_expr)) @@ -180,13 +190,16 @@ impl Parse for ElementAttrValue { } else { let value = input.parse::()?; ElementAttrValue::AttrExpr(value) - }) + }; + + Ok(element_attr_value) } } impl ToTokens for ElementAttrValue { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { + ElementAttrValue::Shorthand(i) => tokens.append_all(quote! { #i }), ElementAttrValue::AttrLiteral(lit) => tokens.append_all(quote! { #lit }), ElementAttrValue::AttrOptionalExpr { condition, value } => { tokens.append_all(quote! { if #condition { Some(#value) } else { None } }) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index e62fc6d6e5..45a11125cc 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -8,6 +8,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, spanned::Spanned, + token::Brace, Expr, Ident, LitStr, Result, Token, }; @@ -59,6 +60,7 @@ impl Parse for Element { } // Parse the raw literal fields + // "def": 456, if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { let name = content.parse::()?; let ident = name.clone(); @@ -84,6 +86,8 @@ impl Parse for Element { continue; } + // Parse + // abc: 123, if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { let name = content.parse::()?; @@ -102,22 +106,17 @@ impl Parse for Element { value: ElementAttrValue::EventTokens(content.parse()?), }, })); + } else if name_str == "key" { + key = Some(content.parse()?); } else { - match name_str.as_str() { - "key" => { - key = Some(content.parse()?); - } - _ => { - let value = content.parse::()?; - attributes.push(attribute::AttributeType::Named(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr { - name: ElementAttrName::BuiltIn(name), - value, - }, - })); - } - } + let value = content.parse::()?; + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value, + }, + })); } if content.is_empty() { @@ -130,6 +129,33 @@ impl Parse for Element { continue; } + // Parse shorthand fields + if content.peek(Ident) + && !content.peek2(Brace) + && !content.peek2(Token![:]) + && !content.peek2(Token![-]) + { + let name = content.parse::()?; + let name_ = name.clone(); + let value = ElementAttrValue::Shorthand(name.clone()); + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value, + }, + })); + + if content.is_empty() { + break; + } + + if content.parse::().is_err() { + missing_trailing_comma!(name_.span()); + } + continue; + } + break; } @@ -137,31 +163,21 @@ impl Parse for Element { // For example, if there are two `class` attributes, combine them into one let mut merged_attributes: Vec = Vec::new(); for attr in &attributes { - if let Some(old_attr_index) = merged_attributes.iter().position(|a| { - matches!((a, attr), ( - AttributeType::Named(ElementAttrNamed { - attr: ElementAttr { - name: ElementAttrName::BuiltIn(old_name), - .. - }, - .. - }), - AttributeType::Named(ElementAttrNamed { - attr: ElementAttr { - name: ElementAttrName::BuiltIn(new_name), - .. - }, - .. - }), - ) if old_name == new_name) - }) { + let attr_index = merged_attributes + .iter() + .position(|a| a.matches_attr_name(attr)); + + if let Some(old_attr_index) = attr_index { let old_attr = &mut merged_attributes[old_attr_index]; + if let Some(combined) = old_attr.try_combine(attr) { *old_attr = combined; } - } else { - merged_attributes.push(attr.clone()); + + continue; } + + merged_attributes.push(attr.clone()); } while !content.is_empty() { diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 70d03fc524..a23c6b0ff4 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -668,9 +668,9 @@ fn diff_template() { p { "hello world" } - (0..10).map(|i| rsx!{"{i}"}), - (0..10).map(|i| rsx!{"{i}"}), - (0..11).map(|i| rsx!{"{i}"}), + {(0..10).map(|i| rsx!{"{i}"})}, + {(0..10).map(|i| rsx!{"{i}"})}, + {(0..11).map(|i| rsx!{"{i}"})}, Comp{} } }; @@ -714,9 +714,9 @@ fn diff_template() { "height2": "100px", width: 100, Comp{} - (0..11).map(|i| rsx!{"{i}"}), - (0..10).map(|i| rsx!{"{i}"}), - (0..10).map(|i| rsx!{"{i}"}), + {(0..11).map(|i| rsx!{"{i}"})}, + {(0..10).map(|i| rsx!{"{i}"})}, + {(0..10).map(|i| rsx!{"{i}"})}, p { "hello world" } From 593527d58bf62695e9429004e98298c1373cec79 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 23:18:47 -0800 Subject: [PATCH 05/11] fix final test --- packages/rsx/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index a23c6b0ff4..ac8186b24f 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -576,7 +576,7 @@ fn create_template() { p { "hello world" } - (0..10).map(|i| rsx!{"{i}"}) + {(0..10).map(|i| rsx!{"{i}"})} } }; From b8061d6d1463d09fe7985e40ea9d3d93d9efad5c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 23:26:26 -0800 Subject: [PATCH 06/11] Make clippy happy --- examples/control_focus.rs | 2 +- examples/scroll_to_top.rs | 6 ++++-- packages/core/src/nodes.rs | 2 +- packages/core/src/scope_context.rs | 8 ++++---- packages/core/tests/create_fragments.rs | 2 +- packages/core/tests/miri_stress.rs | 2 +- packages/rsx/src/component.rs | 19 +++++++------------ packages/ssr/tests/simple.rs | 2 +- 8 files changed, 20 insertions(+), 23 deletions(-) diff --git a/examples/control_focus.rs b/examples/control_focus.rs index ac4e204181..f161652f0e 100644 --- a/examples/control_focus.rs +++ b/examples/control_focus.rs @@ -15,7 +15,7 @@ fn app(cx: Scope) -> Element { if *running.current() { loop { tokio::time::sleep(std::time::Duration::from_millis(10)).await; - if let Some(element) = elements.read().get(focused) { + if let Some(element) = elements.with(|f| f.get(focused).cloned()) { _ = element.set_focus(true).await; } else { focused = 0; diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs index 4159fd67aa..eed0a00b52 100644 --- a/examples/scroll_to_top.rs +++ b/examples/scroll_to_top.rs @@ -22,8 +22,10 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { - if let Some(header) = header_element.read().as_ref() { - _ = header.scroll_to(ScrollBehavior::Smooth); + if let Some(header) = header_element.read().as_ref().cloned() { + cx.spawn(async move { + let _ = header.scroll_to(ScrollBehavior::Smooth).await; + }); } }, "Scroll to top" diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 98964e948b..01812bef57 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -868,7 +868,7 @@ where nodes.extend(self.into_iter().map(|node| node.into_vnode(cx))); match nodes.into_bump_slice() { - children if children.is_empty() => DynamicNode::default(), + [] => DynamicNode::default(), children => DynamicNode::Fragment(children), } } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index ffb9e74a11..b7c6d84049 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -117,7 +117,7 @@ impl ScopeContext { } let mut search_parent = self.parent_id; - match with_runtime(|runtime: &crate::runtime::Runtime| { + let cur_runtime = with_runtime(|runtime: &crate::runtime::Runtime| { while let Some(parent_id) = search_parent { let parent = runtime.get_context(parent_id).unwrap(); tracing::trace!( @@ -135,9 +135,9 @@ impl ScopeContext { search_parent = parent.parent_id; } None - }) - .flatten() - { + }); + + match cur_runtime.flatten() { Some(ctx) => Some(ctx), None => { tracing::trace!( diff --git a/packages/core/tests/create_fragments.rs b/packages/core/tests/create_fragments.rs index fff90464a6..8269320aef 100644 --- a/packages/core/tests/create_fragments.rs +++ b/packages/core/tests/create_fragments.rs @@ -7,7 +7,7 @@ use dioxus_core::ElementId; #[test] fn empty_fragment_creates_nothing() { fn app(cx: Scope) -> Element { - cx.render(rsx!({ () })) + cx.render(rsx!({})) } let mut vdom = VirtualDom::new(app); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index f571b30ced..aad596e823 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -15,7 +15,7 @@ fn test_memory_leak() { cx.spawn(async {}); if val == 2 || val == 4 { - return render!({ () }); + return render!({}); } let name = cx.use_hook(|| String::from("numbers: ")); diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 528a39072a..c7f4c1828c 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -53,20 +53,15 @@ impl Parse for Component { if content.peek(Token![..]) { content.parse::()?; manual_props = Some(content.parse()?); - } else - // - // Fields + } else if // Named fields - if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - fields.push(content.parse()?); - } else - // + (content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:])) // shorthand struct initialization - // Not a div {}, mod::Component {}, or web-component {} - if content.peek(Ident) - && !content.peek2(Brace) - && !content.peek2(Token![:]) - && !content.peek2(Token![-]) + // Not a div {}, mod::Component {}, or web-component {} + || (content.peek(Ident) + && !content.peek2(Brace) + && !content.peek2(Token![:]) + && !content.peek2(Token![-])) { fields.push(content.parse()?); } else { diff --git a/packages/ssr/tests/simple.rs b/packages/ssr/tests/simple.rs index 9e4a2e2b0b..bc90e9ee60 100644 --- a/packages/ssr/tests/simple.rs +++ b/packages/ssr/tests/simple.rs @@ -68,7 +68,7 @@ fn fragments() { dioxus_ssr::render_lazy(rsx! { div { for _ in 0..5 { - {()} + {} } } }), From 8a77d2560e4b41c719208258ef519ce62adcec60 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 23:42:36 -0800 Subject: [PATCH 07/11] fix tests around children elements --- examples/shorthand.rs | 5 ++++- packages/core/src/virtual_dom.rs | 2 +- packages/rsx/src/component.rs | 21 +++++++++++++++------ packages/rsx/src/element.rs | 6 ++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/examples/shorthand.rs b/examples/shorthand.rs index 4a3e9b4161..38b965f3d1 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -11,8 +11,11 @@ fn app(cx: Scope) -> Element { let class = "class"; let id = "id"; + // todo: i'd like it for children to be inferred + let children = render! { "Child" }; + render! { - div { class, id } + div { class, id, {children} } Component { a, b, c } Component { a, ..ComponentProps { a: 1, b: 2, c: 3 } } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 28ccbc76a3..932f9cbfb6 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -78,7 +78,7 @@ use std::{ /// #[component] /// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { /// cx.render(rsx! { -/// div { id: "title", children } +/// div { id: "title", {children} } /// }) /// } /// ``` diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index c7f4c1828c..b878d28218 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -55,14 +55,23 @@ impl Parse for Component { manual_props = Some(content.parse()?); } else if // Named fields - (content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:])) + content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + fields.push(content.parse()?); + } else if // shorthand struct initialization - // Not a div {}, mod::Component {}, or web-component {} - || (content.peek(Ident) - && !content.peek2(Brace) - && !content.peek2(Token![:]) - && !content.peek2(Token![-])) + // Not a div {}, mod::Component {}, or web-component {} + content.peek(Ident) + && !content.peek2(Brace) + && !content.peek2(Token![:]) + && !content.peek2(Token![-]) { + let name = content.fork().parse::().unwrap().to_string(); + + // If the shorthand field is children, these are actually children! + if name == "children" { + panic!("children must be wrapped in braces"); + }; + fields.push(content.parse()?); } else { children.push(content.parse()?); diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 45a11125cc..ff398f7d34 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -137,6 +137,12 @@ impl Parse for Element { { let name = content.parse::()?; let name_ = name.clone(); + + // If the shorthand field is children, these are actually children! + if name == "children" { + panic!("children must be wrapped in braces"); + }; + let value = ElementAttrValue::Shorthand(name.clone()); attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), From aff52596542dca057f2e2d1b2ee9d0926a9f5aee Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 10 Jan 2024 23:52:38 -0800 Subject: [PATCH 08/11] Allow children shorthand in components --- examples/shorthand.rs | 9 +++++---- packages/rsx/src/component.rs | 21 ++++++--------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/examples/shorthand.rs b/examples/shorthand.rs index 38b965f3d1..09214147f4 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -15,17 +15,18 @@ fn app(cx: Scope) -> Element { let children = render! { "Child" }; render! { - div { class, id, {children} } - Component { a, b, c } - Component { a, ..ComponentProps { a: 1, b: 2, c: 3 } } + div { class, id, {&children} } + Component { a, b, c, children } + Component { a, ..ComponentProps { a: 1, b: 2, c: 3, children: None } } } } #[component] -fn Component(cx: Scope, a: i32, b: i32, c: i32) -> Element { +fn Component<'a>(cx: Scope<'a>, a: i32, b: i32, c: i32, children: Element<'a>) -> Element { render! { div { "{a}" } div { "{b}" } div { "{c}" } + div { {children} } } } diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index b878d28218..c7f4c1828c 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -55,23 +55,14 @@ impl Parse for Component { manual_props = Some(content.parse()?); } else if // Named fields - content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - fields.push(content.parse()?); - } else if + (content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:])) // shorthand struct initialization - // Not a div {}, mod::Component {}, or web-component {} - content.peek(Ident) - && !content.peek2(Brace) - && !content.peek2(Token![:]) - && !content.peek2(Token![-]) + // Not a div {}, mod::Component {}, or web-component {} + || (content.peek(Ident) + && !content.peek2(Brace) + && !content.peek2(Token![:]) + && !content.peek2(Token![-])) { - let name = content.fork().parse::().unwrap().to_string(); - - // If the shorthand field is children, these are actually children! - if name == "children" { - panic!("children must be wrapped in braces"); - }; - fields.push(content.parse()?); } else { children.push(content.parse()?); From 8881b8a47386029a7de048185bcfc88e58dfe3a8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 11 Jan 2024 11:37:12 -0800 Subject: [PATCH 09/11] Use error instead of panic for span location in rsx macro shorthand --- packages/rsx/src/element.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index ff398f7d34..b3fb3cf4f8 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -140,7 +140,17 @@ impl Parse for Element { // If the shorthand field is children, these are actually children! if name == "children" { - panic!("children must be wrapped in braces"); + return Err(syn::Error::new( + name.span(), + format!( + r#"Shorthand element children are not supported. +To pass children into elements, wrap them in curly braces. +Like so: + div {{ {{children}} }} + +"#, + ), + )); }; let value = ElementAttrValue::Shorthand(name.clone()); From 1527b81e02541c1040fd61f6f094b1cb2353a78a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 11 Jan 2024 12:11:27 -0800 Subject: [PATCH 10/11] Components participate in event handlers --- examples/drops.rs | 4 ++-- examples/dynamic_asset.rs | 8 +++----- examples/error_handle.rs | 8 +++----- examples/file_explorer.rs | 11 +++++------ examples/generic_component.rs | 14 ++++++-------- examples/hello_world.rs | 4 ++-- examples/pattern_model.rs | 4 +++- examples/shared_state.rs | 29 +++++++++++++---------------- examples/shorthand.rs | 21 +++++++++++++++++---- packages/core/src/nodes.rs | 15 +++++++++++++++ packages/rsx/src/attribute.rs | 19 ++++++++++++++----- packages/rsx/src/component.rs | 3 +++ 12 files changed, 86 insertions(+), 54 deletions(-) diff --git a/examples/drops.rs b/examples/drops.rs index 956545e350..6328a3f647 100644 --- a/examples/drops.rs +++ b/examples/drops.rs @@ -14,9 +14,9 @@ fn app(cx: Scope) -> Element { } render! { - {(0..count).map(|_| rsx!{ + for _ in 0..count { drop_child {} - })} + } } } diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs index 51dcfac3aa..ac2ad82c1f 100644 --- a/examples/dynamic_asset.rs +++ b/examples/dynamic_asset.rs @@ -17,11 +17,9 @@ fn app(cx: Scope) -> Element { response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec())); }); - cx.render(rsx! { + render! { div { - img { - src: "/logos/logo.png" - } + img { src: "/logos/logo.png" } } - }) + } } diff --git a/examples/error_handle.rs b/examples/error_handle.rs index 21b16ec453..5461032fdc 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -22,9 +22,7 @@ fn DemoC(cx: Scope, x: i32) -> Element { result.throw()?; - cx.render(rsx! { - h1 { - "{x}" - } - }) + render! { + h1 { "{x}" } + } } diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index a99ac3232a..ea89fab77c 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -35,18 +35,17 @@ fn app(cx: Scope) -> Element { main { {files.read().path_names.iter().enumerate().map(|(dir_id, path)| { let path_end = path.split('/').last().unwrap_or(path.as_str()); - let icon_type = if path_end.contains('.') { - "description" - } else { - "folder" - }; rsx! ( div { class: "folder", key: "{path}", i { class: "material-icons", onclick: move |_| files.write().enter_dir(dir_id), - "{icon_type}" + if path_end.contains('.') { + "description" + } else { + "folder" + } p { class: "cooltip", "0 folders / 0 files" } } h1 { "{path_end}" } diff --git a/examples/generic_component.rs b/examples/generic_component.rs index 3d6171a9bf..ec8eeb660d 100644 --- a/examples/generic_component.rs +++ b/examples/generic_component.rs @@ -7,9 +7,9 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(rsx! { generic_child { - data: 0i32 - } }) + render! { + generic_child { data: 0 } + } } #[derive(PartialEq, Props)] @@ -18,9 +18,7 @@ struct GenericChildProps { } fn generic_child(cx: Scope>) -> Element { - let data = &cx.props.data; - - cx.render(rsx! { div { - "{data}" - } }) + render! { + div { "{&cx.props.data}" } + } } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 9553f5c7e8..52d9e9886d 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -5,7 +5,7 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(rsx! ( + render! { div { "Hello, world!" } - )) + } } diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index 13b5409e10..6dd3e88259 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -35,11 +35,13 @@ fn main() { dioxus_desktop::launch_cfg(app, cfg); } +const STYLE: &str = include_str!("./assets/calculator.css"); + fn app(cx: Scope) -> Element { let state = use_ref(cx, Calculator::new); cx.render(rsx! { - style { {include_str!("./assets/calculator.css")} } + style { {STYLE} } div { id: "wrapper", div { class: "app", div { class: "calculator", onkeypress: move |evt| state.write().handle_keydown(evt), diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 487aa3fe3e..65ec76a714 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -51,26 +51,23 @@ pub fn App(cx: Scope) -> Element { #[component] fn DataEditor(cx: Scope, id: usize) -> Element { - let cool_data = use_shared_state::(cx).unwrap().read(); + let data = use_shared_state::(cx)?; - let my_data = &cool_data.view(id).unwrap(); - - render!(p { - "{my_data}" - }) + render! { + p { + {data.read().view(id)?} + } + } } #[component] fn DataView(cx: Scope, id: usize) -> Element { - let cool_data = use_shared_state::(cx).unwrap(); - - let oninput = |e: FormEvent| cool_data.write().set(*id, e.value()); + let data = use_shared_state::(cx)?; - let cool_data = cool_data.read(); - let my_data = &cool_data.view(id).unwrap(); - - render!(input { - oninput: oninput, - value: "{my_data}" - }) + render! { + input { + oninput: move |e: FormEvent| data.write().set(*id, e.value()), + value: data.read().view(id)? + } + } } diff --git a/examples/shorthand.rs b/examples/shorthand.rs index 09214147f4..2d71b046b1 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -11,22 +11,35 @@ fn app(cx: Scope) -> Element { let class = "class"; let id = "id"; - // todo: i'd like it for children to be inferred + // todo: i'd like it for children on elements to be inferred as the children of the element + // also should shorthands understand references/dereferences? + // ie **a, *a, &a, &mut a, etc let children = render! { "Child" }; + let onclick = move |_| println!("Clicked!"); render! { div { class, id, {&children} } - Component { a, b, c, children } - Component { a, ..ComponentProps { a: 1, b: 2, c: 3, children: None } } + Component { a, b, c, children, onclick } + Component { a, ..ComponentProps { a: 1, b: 2, c: 3, children: None, onclick: Default::default() } } } } #[component] -fn Component<'a>(cx: Scope<'a>, a: i32, b: i32, c: i32, children: Element<'a>) -> Element { +fn Component<'a>( + cx: Scope<'a>, + a: i32, + b: i32, + c: i32, + children: Element<'a>, + onclick: EventHandler<'a, ()>, +) -> Element { render! { div { "{a}" } div { "{b}" } div { "{c}" } div { {children} } + div { + onclick: move |_| onclick.call(()), + } } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 01812bef57..f30fdbcf3a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -804,6 +804,15 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str { } } +impl IntoDynNode<'_> for &String { + fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode { + DynamicNode::Text(VText { + value: cx.bump().alloc_str(&self), + id: Default::default(), + }) + } +} + impl IntoDynNode<'_> for String { fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode { DynamicNode::Text(VText { @@ -898,6 +907,12 @@ impl<'a> IntoAttributeValue<'a> for String { } } +impl<'a> IntoAttributeValue<'a> for &String { + fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Text(cx.alloc_str(&self)) + } +} + impl<'a> IntoAttributeValue<'a> for f64 { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Float(self) diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index c5d3383b0b..f3df1d1c0b 100644 --- a/packages/rsx/src/attribute.rs +++ b/packages/rsx/src/attribute.rs @@ -109,17 +109,25 @@ impl ToTokens for ElementAttrNamed { }; let attribute = { + let value = &self.attr.value; + let is_shorthand_event = match &attr.value { + ElementAttrValue::Shorthand(s) => s.to_string().starts_with("on"), + _ => false, + }; + match &attr.value { ElementAttrValue::AttrLiteral(_) | ElementAttrValue::AttrExpr(_) | ElementAttrValue::Shorthand(_) - | ElementAttrValue::AttrOptionalExpr { .. } => { + | ElementAttrValue::AttrOptionalExpr { .. } + if !is_shorthand_event => + { let name = &self.attr.name; let ns = ns(name); let volitile = volitile(name); let attribute = attribute(name); - let value = &self.attr.value; let value = quote! { #value }; + quote! { __cx.attr( #attribute, @@ -131,12 +139,13 @@ impl ToTokens for ElementAttrNamed { } ElementAttrValue::EventTokens(tokens) => match &self.attr.name { ElementAttrName::BuiltIn(name) => { - quote! { - dioxus_elements::events::#name(__cx, #tokens) - } + quote! { dioxus_elements::events::#name(__cx, #tokens) } } ElementAttrName::Custom(_) => todo!(), }, + _ => { + quote! { dioxus_elements::events::#value(__cx, #value) } + } } }; diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index c7f4c1828c..3fef27539a 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -241,6 +241,9 @@ impl ContentField { impl ToTokens for ContentField { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { + ContentField::Shorthand(i) if i.to_string().starts_with("on") => { + tokens.append_all(quote! { __cx.event_handler(#i) }) + } ContentField::Shorthand(i) => tokens.append_all(quote! { #i }), ContentField::ManExpr(e) => e.to_tokens(tokens), ContentField::Formatted(s) => tokens.append_all(quote! { __cx.raw_text(#s) }), From 8ff13c3c1b0331b21387ea617c2b7bf95efedc14 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 11 Jan 2024 12:13:38 -0800 Subject: [PATCH 11/11] Fix some basic clippy stuff --- packages/core/src/nodes.rs | 4 ++-- packages/rsx/src/element.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index f30fdbcf3a..dc9c85fb10 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -807,7 +807,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str { impl IntoDynNode<'_> for &String { fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode { DynamicNode::Text(VText { - value: cx.bump().alloc_str(&self), + value: cx.bump().alloc_str(self), id: Default::default(), }) } @@ -909,7 +909,7 @@ impl<'a> IntoAttributeValue<'a> for String { impl<'a> IntoAttributeValue<'a> for &String { fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> { - AttributeValue::Text(cx.alloc_str(&self)) + AttributeValue::Text(cx.alloc_str(self)) } } diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index b3fb3cf4f8..7d7a9a3c83 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -142,14 +142,12 @@ impl Parse for Element { if name == "children" { return Err(syn::Error::new( name.span(), - format!( - r#"Shorthand element children are not supported. + r#"Shorthand element children are not supported. To pass children into elements, wrap them in curly braces. Like so: div {{ {{children}} }} "#, - ), )); };