From 5180a68bce7a3c58400d8a469aba01b260d492ca Mon Sep 17 00:00:00 2001 From: "Matt \"Siyuan\" Yan" Date: Thu, 9 Apr 2026 13:42:48 +0900 Subject: [PATCH 1/6] feat: All-you-can-inline html! macro overhaul - Support unbraced match arms and let bindings in match arm bodies - Support bare literals and expressions in unbraced match arms and for-loops - Support bare nodes and let bindings in if/else bodies - Support `let` bindings in `for` bodies - Automatic root fragment wrapping - Deny patterns containing unnecessarily nested `html!` macros and unnecessary fragments - Update examples to use the new syntax - Document match expressions, let bindings, and bare nodes in html! --- examples/boids/src/main.rs | 8 +- examples/counter_functional/src/main.rs | 8 +- .../dyn_create_destroy_apps/src/counter.rs | 22 +- examples/dyn_create_destroy_apps/src/main.rs | 26 +- examples/function_delayed_input/src/main.rs | 8 +- .../src/components/chessboard.rs | 11 +- .../src/components/game_status_board.rs | 29 +- examples/function_router/src/app.rs | 26 +- .../src/components/pagination.rs | 59 +- examples/function_router/src/pages/author.rs | 4 +- .../function_router/src/pages/author_list.rs | 18 +- examples/function_router/src/pages/home.rs | 56 +- examples/function_router/src/pages/post.rs | 68 +- examples/function_todomvc/src/main.rs | 29 +- examples/function_todomvc/src/state.rs | 2 +- examples/futures/src/main.rs | 16 +- examples/game_of_life/src/main.rs | 24 +- examples/immutable/src/array.rs | 12 +- examples/immutable/src/main.rs | 18 +- examples/immutable/src/map.rs | 8 +- examples/immutable/src/string.rs | 2 - examples/js_callback/src/main.rs | 12 +- examples/keyed_list/src/main.rs | 174 +++-- examples/nested_list/src/main.rs | 2 +- examples/portals/src/main.rs | 12 +- examples/router/src/components/pagination.rs | 50 +- examples/router/src/main.rs | 26 +- examples/router/src/pages/author.rs | 4 +- examples/router/src/pages/author_list.rs | 18 +- examples/router/src/pages/home.rs | 56 +- examples/router/src/pages/post.rs | 68 +- examples/timer/src/main.rs | 38 +- examples/timer_functional/src/main.rs | 22 +- examples/todomvc/src/state.rs | 2 +- examples/wasi_ssr_module/src/main.rs | 6 +- examples/wasi_ssr_module/src/router.rs | 14 +- examples/web_worker_fib/src/lib.rs | 22 +- examples/web_worker_prime/src/lib.rs | 18 +- .../yew-macro/src/html_tree/html_block.rs | 67 +- packages/yew-macro/src/html_tree/html_for.rs | 31 +- packages/yew-macro/src/html_tree/html_list.rs | 7 + .../yew-macro/src/html_tree/html_match.rs | 219 ++++++ packages/yew-macro/src/html_tree/html_node.rs | 4 +- packages/yew-macro/src/html_tree/mod.rs | 143 +++- packages/yew-macro/tests/derive_props/pass.rs | 4 +- .../yew-macro/tests/html_macro/block-fail.rs | 30 +- .../tests/html_macro/block-fail.stderr | 35 +- .../yew-macro/tests/html_macro/block-pass.rs | 6 +- .../html_macro/component-any-children-pass.rs | 170 +++-- .../tests/html_macro/component-fail.rs | 6 - .../tests/html_macro/component-fail.stderr | 82 +-- .../tests/html_macro/component-pass.rs | 182 +++-- .../html_macro/custom-type-in-block-fail.rs | 2 +- .../tests/html_macro/element-fail.rs | 2 - .../tests/html_macro/element-fail.stderr | 212 +++--- .../yew-macro/tests/html_macro/for-fail.rs | 5 + .../tests/html_macro/for-fail.stderr | 6 + .../yew-macro/tests/html_macro/for-pass.rs | 47 +- .../tests/html_macro/html-if-fail.rs | 8 + .../tests/html_macro/html-if-fail.stderr | 12 + .../tests/html_macro/html-if-pass.rs | 132 +++- .../tests/html_macro/html-match-fail.rs | 36 + .../tests/html_macro/html-match-fail.stderr | 35 + .../tests/html_macro/html-match-pass.rs | 315 +++++++++ .../tests/html_macro/iterable-fail.rs | 4 +- .../tests/html_macro/iterable-fail.stderr | 4 +- .../tests/html_macro/iterable-pass.rs | 12 +- .../yew-macro/tests/html_macro/list-fail.rs | 12 +- .../tests/html_macro/list-fail.stderr | 46 +- .../yew-macro/tests/html_macro/list-pass.rs | 13 +- .../tests/html_macro/multi-root-pass.rs | 63 ++ .../yew-macro/tests/html_macro/node-fail.rs | 1 - .../tests/html_macro/node-fail.stderr | 30 +- packages/yew-router/tests/basename.rs | 18 +- packages/yew-router/tests/browser_router.rs | 18 +- packages/yew-router/tests/hash_router.rs | 18 +- .../yew-router/tests/url_encoded_routes.rs | 10 +- packages/yew/src/dom_bundle/bcomp.rs | 2 +- packages/yew/src/dom_bundle/blist.rs | 636 +++++++----------- packages/yew/src/dom_bundle/bportal.rs | 2 +- packages/yew/src/dom_bundle/btag/listeners.rs | 18 +- packages/yew/src/dom_bundle/btag/mod.rs | 95 +-- packages/yew/src/dom_bundle/btext.rs | 12 +- .../yew/src/functional/hooks/use_callback.rs | 2 +- .../yew/src/functional/hooks/use_effect.rs | 2 +- .../yew/src/functional/hooks/use_reducer.rs | 8 +- packages/yew/src/html/component/marker.rs | 4 +- packages/yew/src/suspense/component.rs | 4 +- packages/yew/src/virtual_dom/vlist.rs | 8 +- packages/yew/tests/hydration.rs | 26 +- packages/yew/tests/layout.rs | 12 +- packages/yew/tests/raw_html.rs | 30 +- packages/yew/tests/suspense.rs | 2 +- packages/yew/tests/use_context.rs | 20 +- packages/yew/tests/use_reducer.rs | 24 +- packages/yew/tests/use_state.rs | 16 +- tools/benchmark-ssr/src/main.rs | 10 +- website/docs/advanced-topics/children.mdx | 8 +- .../advanced-topics/server-side-rendering.mdx | 4 +- .../concepts/basic-web-technologies/html.mdx | 33 +- .../function-components/properties.mdx | 10 +- website/docs/concepts/html/components.mdx | 20 +- .../concepts/html/conditional-rendering.mdx | 86 ++- website/docs/concepts/html/events.mdx | 108 ++- website/docs/concepts/html/fragments.mdx | 37 +- website/docs/concepts/html/introduction.mdx | 24 +- website/docs/concepts/html/lists.mdx | 14 + .../html/literals-and-expressions.mdx | 33 +- website/docs/concepts/router.mdx | 8 +- website/docs/tutorial/index.mdx | 125 ++-- .../current/advanced-topics/children.mdx | 8 +- .../advanced-topics/server-side-rendering.mdx | 4 +- .../concepts/basic-web-technologies/html.mdx | 31 +- .../function-components/properties.mdx | 10 +- .../current/concepts/html/components.mdx | 20 +- .../concepts/html/conditional-rendering.mdx | 86 ++- .../current/concepts/html/events.mdx | 108 ++- .../current/concepts/html/fragments.mdx | 35 +- .../current/concepts/html/introduction.mdx | 9 +- .../current/concepts/html/lists.mdx | 14 + .../html/literals-and-expressions.mdx | 33 +- .../current/concepts/router.mdx | 12 +- .../current/tutorial/index.mdx | 110 ++- .../current/advanced-topics/children.mdx | 8 +- .../advanced-topics/server-side-rendering.mdx | 4 +- .../concepts/basic-web-technologies/html.mdx | 31 +- .../function-components/properties.mdx | 10 +- .../current/concepts/html/components.mdx | 20 +- .../concepts/html/conditional-rendering.mdx | 86 ++- .../current/concepts/html/events.mdx | 108 ++- .../current/concepts/html/fragments.mdx | 35 +- .../current/concepts/html/introduction.mdx | 9 +- .../current/concepts/html/lists.mdx | 14 + .../html/literals-and-expressions.mdx | 33 +- .../current/concepts/router.mdx | 12 +- .../current/tutorial/index.mdx | 110 ++- .../current/advanced-topics/children.mdx | 8 +- .../advanced-topics/server-side-rendering.mdx | 4 +- .../concepts/basic-web-technologies/html.mdx | 31 +- .../function-components/properties.mdx | 10 +- .../current/concepts/html/components.mdx | 20 +- .../concepts/html/conditional-rendering.mdx | 86 ++- .../current/concepts/html/events.mdx | 108 ++- .../current/concepts/html/fragments.mdx | 35 +- .../current/concepts/html/introduction.mdx | 9 +- .../current/concepts/html/lists.mdx | 14 + .../html/literals-and-expressions.mdx | 33 +- .../current/concepts/router.mdx | 12 +- .../current/tutorial/index.mdx | 110 ++- 149 files changed, 3357 insertions(+), 2571 deletions(-) create mode 100644 packages/yew-macro/src/html_tree/html_match.rs create mode 100644 packages/yew-macro/tests/html_macro/html-match-fail.rs create mode 100644 packages/yew-macro/tests/html_macro/html-match-fail.stderr create mode 100644 packages/yew-macro/tests/html_macro/html-match-pass.rs create mode 100644 packages/yew-macro/tests/html_macro/multi-root-pass.rs diff --git a/examples/boids/src/main.rs b/examples/boids/src/main.rs index bfec32fd6f3..c2edcba1b1d 100644 --- a/examples/boids/src/main.rs +++ b/examples/boids/src/main.rs @@ -66,11 +66,9 @@ impl Component for App { } = *self; html! { - <> -

{ "Boids" }

- - { self.view_panel(ctx.link()) } - +

{ "Boids" }

+ + { self.view_panel(ctx.link()) } } } } diff --git a/examples/counter_functional/src/main.rs b/examples/counter_functional/src/main.rs index 54dcd2806d1..020319cf04e 100644 --- a/examples/counter_functional/src/main.rs +++ b/examples/counter_functional/src/main.rs @@ -15,11 +15,9 @@ fn App() -> Html { }; html! { - <> -

{"current count: "} {*state}

- - - +

{"current count: "} {*state}

+ + } } diff --git a/examples/dyn_create_destroy_apps/src/counter.rs b/examples/dyn_create_destroy_apps/src/counter.rs index 3154c19fa8d..4ff1aca1371 100644 --- a/examples/dyn_create_destroy_apps/src/counter.rs +++ b/examples/dyn_create_destroy_apps/src/counter.rs @@ -44,19 +44,17 @@ impl Component for CounterModel { let destroy_callback = ctx.props().destroy_callback.clone(); html! { - <> - // Display the current value of the counter -

- { "App has lived for " } - { self.counter } - { " ticks" } -

+ // Display the current value of the counter +

+ { "App has lived for " } + { self.counter } + { " ticks" } +

- // Add button to send a destroy command to the parent app - - + // Add button to send a destroy command to the parent app + } } diff --git a/examples/dyn_create_destroy_apps/src/main.rs b/examples/dyn_create_destroy_apps/src/main.rs index 1dd0408f2ec..da5360fdd89 100644 --- a/examples/dyn_create_destroy_apps/src/main.rs +++ b/examples/dyn_create_destroy_apps/src/main.rs @@ -89,20 +89,18 @@ impl Component for App { // We will only render once, and then do the rest of the DOM changes // by mounting/destroying appinstances of CounterModel html! { - <> -
- // Create button to create a new app - -
- // Create a container for all the app instances -
-
- +
+ // Create button to create a new app + +
+ // Create a container for all the app instances +
+
} } } diff --git a/examples/function_delayed_input/src/main.rs b/examples/function_delayed_input/src/main.rs index aaa3a639995..d73de799161 100644 --- a/examples/function_delayed_input/src/main.rs +++ b/examples/function_delayed_input/src/main.rs @@ -68,13 +68,13 @@ fn App() -> Html {
-

{ +

match &*search { - Search::Idle => "Type something to search...".into(), - Search::Fetching(query) => format!("Searching for: {}", query).into(), + Search::Idle => "Type something to search...", + Search::Fetching(query) => format!("Searching for: {}", query), Search::Fetched(response) => response.clone(), } - }

+

diff --git a/examples/function_memory_game/src/components/chessboard.rs b/examples/function_memory_game/src/components/chessboard.rs index 5af83feb1ea..cfa8ffd7d6c 100644 --- a/examples/function_memory_game/src/components/chessboard.rs +++ b/examples/function_memory_game/src/components/chessboard.rs @@ -1,5 +1,4 @@ use yew::prelude::*; -use yew::{Properties, function_component, html}; use crate::components::chessboard_card::ChessboardCard; use crate::state::{Card, RawCard}; @@ -9,15 +8,13 @@ pub struct Props { pub cards: Vec, pub on_flip: Callback, } -#[function_component] +#[component] pub fn Chessboard(props: &Props) -> Html { html! {
- { for props.cards.iter().map(|card| - html! { - - } - ) } + for card in &props.cards { + + }
} } diff --git a/examples/function_memory_game/src/components/game_status_board.rs b/examples/function_memory_game/src/components/game_status_board.rs index 1b6889ad32b..c58afeff295 100644 --- a/examples/function_memory_game/src/components/game_status_board.rs +++ b/examples/function_memory_game/src/components/game_status_board.rs @@ -12,28 +12,19 @@ pub struct Props { #[function_component] pub fn GameStatusBoard(props: &Props) -> Html { - let get_content = { - let onclick = props.on_reset.reform(move |e: MouseEvent| { - e.stop_propagation(); - e.prevent_default(); - }); - + html! { +
match props.status { - Status::Ready => html! { - {"Ready"} - }, - Status::Playing => html! { - {"Playing"} - }, - Status::Passed => html! { + Status::Ready => {"Ready"}, + Status::Playing => {"Playing"}, + Status::Passed => { + let onclick = props.on_reset.reform(move |e: MouseEvent| { + e.stop_propagation(); + e.prevent_default(); + }); - }, + } } - }; - - html! { -
- {get_content} { props.sec_past}{" s"}
} diff --git a/examples/function_router/src/app.rs b/examples/function_router/src/app.rs index 8b7c5a574f4..97dfb126d32 100644 --- a/examples/function_router/src/app.rs +++ b/examples/function_router/src/app.rs @@ -94,24 +94,14 @@ pub fn ServerApp(props: &ServerAppProps) -> Html { } fn switch(routes: Route) -> Html { - match routes { - Route::Post { id } => { - html! { } - } - Route::Posts => { - html! { } - } - Route::Author { id } => { - html! { } - } - Route::Authors => { - html! { } - } - Route::Home => { - html! { } - } - Route::NotFound => { - html! { } + html! { + match routes { + Route::Post { id } => , + Route::Posts => , + Route::Author { id } => , + Route::Authors => , + Route::Home => , + Route::NotFound => , } } } diff --git a/examples/function_router/src/components/pagination.rs b/examples/function_router/src/components/pagination.rs index b217c4de036..e769902fad3 100644 --- a/examples/function_router/src/components/pagination.rs +++ b/examples/function_router/src/components/pagination.rs @@ -29,24 +29,22 @@ pub fn RelNavButtons(props: &Props) -> Html { } = props.clone(); html! { - <> - - classes={classes!("pagination-previous")} - disabled={page==1} - query={Some(PageQuery{page: page - 1})} - to={to.clone()} - > - { "Previous" } - > - - classes={classes!("pagination-next")} - disabled={page==total_pages} - query={Some(PageQuery{page: page + 1})} - {to} - > - { "Next page" } - > - + + classes={classes!("pagination-previous")} + disabled={page==1} + query={Some(PageQuery{page: page - 1})} + to={to.clone()} + > + { "Previous" } + > + + classes={classes!("pagination-next")} + disabled={page==total_pages} + query={Some(PageQuery{page: page + 1})} + {to} + > + { "Next page" } + > } } @@ -70,18 +68,13 @@ pub fn RenderLinks(props: &RenderLinksProps) -> Html { let mut range = range; if len > max_links { - let last_link = - html! {}; - // remove 1 for the ellipsis and 1 for the last link - let links = range - .take(max_links - 2) - .map(|page| html! {}); + let last_page = range.next_back().unwrap(); html! { - <> - { for links } -
  • { ELLIPSIS }
  • - { last_link } - + for page in range.take(max_links - 2) { + + } +
  • { ELLIPSIS }
  • + } } else { html! { @@ -140,11 +133,9 @@ pub fn Links(props: &Props) -> Html { let links_right = 2 * LINKS_PER_SIDE - links_left; html! { - <> - - - - + + + } } diff --git a/examples/function_router/src/pages/author.rs b/examples/function_router/src/pages/author.rs index 97cb2f4cd25..fa1a0ca2867 100644 --- a/examples/function_router/src/pages/author.rs +++ b/examples/function_router/src/pages/author.rs @@ -41,7 +41,9 @@ pub fn Author(props: &Props) -> Html {

    { "Interests" }

    - { for author.keywords.iter().map(|tag| html! { { tag } }) } + for tag in &author.keywords { + { tag } + }
    diff --git a/examples/function_router/src/pages/author_list.rs b/examples/function_router/src/pages/author_list.rs index 82874be6218..aa8944d8511 100644 --- a/examples/function_router/src/pages/author_list.rs +++ b/examples/function_router/src/pages/author_list.rs @@ -11,16 +11,6 @@ const CAROUSEL_DELAY_MS: u32 = 15000; pub fn AuthorList() -> Html { let seeds = use_state(random_author_seeds); - let authors = seeds.iter().map(|&seed| { - html! { -
    -
    - -
    -
    - } - }); - let on_complete = { let seeds = seeds.clone(); @@ -50,7 +40,13 @@ pub fn AuthorList() -> Html {

    - { for authors } + for seed in seeds.iter() { +
    +
    + +
    +
    + }
    diff --git a/examples/function_router/src/pages/home.rs b/examples/function_router/src/pages/home.rs index 949913b345a..f43d6528907 100644 --- a/examples/function_router/src/pages/home.rs +++ b/examples/function_router/src/pages/home.rs @@ -3,42 +3,40 @@ use yew::prelude::*; #[function_component] fn InfoTiles() -> Html { html! { - <> -
    -
    -

    { "What are yews?" }

    -

    { "Everything you need to know!" }

    +
    +
    +

    { "What are yews?" }

    +

    { "Everything you need to know!" }

    -
    - {r#" - A yew is a small to medium-sized evergreen tree, growing 10 to 20 metres tall, with a trunk up to 2 metres in diameter. - The bark is thin, scaly brown, coming off in small flakes aligned with the stem. - The leaves are flat, dark green, 1 to 4 centimetres long and 2 to 3 millimetres broad, arranged spirally on the stem, - but with the leaf bases twisted to align the leaves in two flat rows either side of the stem, - except on erect leading shoots where the spiral arrangement is more obvious. - The leaves are poisonous. - "#} -
    +
    + {r#" + A yew is a small to medium-sized evergreen tree, growing 10 to 20 metres tall, with a trunk up to 2 metres in diameter. + The bark is thin, scaly brown, coming off in small flakes aligned with the stem. + The leaves are flat, dark green, 1 to 4 centimetres long and 2 to 3 millimetres broad, arranged spirally on the stem, + but with the leaf bases twisted to align the leaves in two flat rows either side of the stem, + except on erect leading shoots where the spiral arrangement is more obvious. + The leaves are poisonous. + "#}
    +
    -
    -
    -

    { "Who are we?" }

    +
    +
    +

    { "Who are we?" }

    -
    - { "We're a small team of just 2" } - { 64 } - { " members working tirelessly to bring you the low-effort yew content we all desperately crave." } -
    - {r#" - We put a ton of effort into fact-checking our posts. - Some say they read like a Wikipedia article - what a compliment! - "#} -
    +
    + { "We're a small team of just 2" } + { 64 } + { " members working tirelessly to bring you the low-effort yew content we all desperately crave." } +
    + {r#" + We put a ton of effort into fact-checking our posts. + Some say they read like a Wikipedia article - what a compliment! + "#}
    - +
    } } diff --git a/examples/function_router/src/pages/post.rs b/examples/function_router/src/pages/post.rs index a41d856e37b..dc12a0a769d 100644 --- a/examples/function_router/src/pages/post.rs +++ b/examples/function_router/src/pages/post.rs @@ -83,20 +83,16 @@ pub fn Post(props: &Props) -> Html { }; let render_section = |section, show_hero| { - let hero = if show_hero { - render_section_hero(section) - } else { - html! {} - }; - let paragraphs = section.paragraphs.iter().map(|paragraph| { - html! { -

    { paragraph }

    - } - }); html! {
    - { hero } -
    { for paragraphs }
    + if show_hero { + render_section_hero(section) + } +
    + for paragraph in §ion.paragraphs { +

    { paragraph }

    + } +
    } }; @@ -121,36 +117,30 @@ pub fn Post(props: &Props) -> Html { html! {{for parts}} }; - let keywords = post - .meta - .keywords - .iter() - .map(|keyword| html! { { keyword } }); - html! { - <> -
    - The hero's background -
    -
    -

    - { &post.meta.title } -

    -

    - { "by " } - classes={classes!("has-text-weight-semibold")} to={Route::Author { id: post.meta.author.seed }}> - { &post.meta.author.name } - > -

    -
    - { for keywords } -
    +
    + The hero's background +
    +
    +

    + { &post.meta.title } +

    +

    + { "by " } + classes={classes!("has-text-weight-semibold")} to={Route::Author { id: post.meta.author.seed }}> + { &post.meta.author.name } + > +

    +
    + for keyword in &post.meta.keywords { + { keyword } + }
    -
    -
    - { view_content }
    - +
    +
    + { view_content } +
    } } diff --git a/examples/function_todomvc/src/main.rs b/examples/function_todomvc/src/main.rs index 9c5028fcd41..b5be763be0c 100644 --- a/examples/function_todomvc/src/main.rs +++ b/examples/function_todomvc/src/main.rs @@ -80,14 +80,13 @@ fn app() -> Html { />