Skip to content

feat: first-class match, let bindings, and bare nodes in html! control flow#4118

Draft
Madoshakalaka wants to merge 13 commits intomasterfrom
feat/html-match-and-let-in-for
Draft

feat: first-class match, let bindings, and bare nodes in html! control flow#4118
Madoshakalaka wants to merge 13 commits intomasterfrom
feat/html-match-and-let-in-for

Conversation

@Madoshakalaka
Copy link
Copy Markdown
Member

@Madoshakalaka Madoshakalaka commented Apr 6, 2026

Description

Add first-class match expression support and let bindings in for loop bodies to the html! macro.

First-class match:

Previously, match required a block wrapper and nested html! calls per arm:

html! {
    <div>{
        match status {
            Status::Loading => html! { <Spinner /> },
            Status::Ready(data) => html! { <DataView data={data} /> },
        }
    }</div>
}

Now match works directly, following the same pattern as the existing if/else support, with let binding allowed:

html! {
    match status {
        Status::Ready(data) => {
            let data_pretty = format!("My data: {data}");
            let class = if data.important { "highlight" } else { "normal" };
            <DataView data={data_pretty} class={class} />
        }
        _ => <Spinner/>
    }
}

As shown above, match arms with a single element doesn't require braces:

html! {
    match status {
        Status::Loading => <Spinner/>,
        Status::Error(e) => <p class="error">{e}</p>,
        Status::Ready(data) => {
            <DataView data={data} />
        }
    }
}

Note, even the commas are omittable but due to users' familarity with Rust match statements, I have preserved the commas in code examples for now.

let bindings in for bodies:

Previously, let bindings inside for loops required a nested block with an inner html! call:

html! {
    for item in items {
        {{ let processed = transform(&item); html! { <div>{processed.name}</div> } }}
    }
}

Now let bindings can appear directly before html children:

html! {
    for item in items {
        let processed = transform(&item);
        let class = if processed.active { "active" } else { "inactive" };
        <div class={class}>{processed.name}</div>
    }
}

let bindings must appear before any html children (not interleaved). This avoids parsing ambiguity since let is not a valid start for any HtmlTree variant. The bindings are emitted inside the Iterator::for_each closure, scoped to each iteration.

If statements let-binding and bare return node support

This now just works:

html! {
    if condition {
        let label = format!("count: {count}");
        <span>{label}</span>
    } else {
        "nothing"
    }
}

Note:

Both worked on master:

html! { "foo" }
html! { <div>{"foo"}</div>  }

however, only the first of the two below worked:

html!{
  if foo {
    <div>{"foo"}</div>
  }
}
html!{
  if foo {
    "foo"
  }
}

I consider this a bug, which is fixed now.

Multi-children arms, multi-children for-loop bodies, and multi-children if-else bodies

Example:

html!{
  if foo {
    <span>{"123"}</span>
    <span>{"234"}</span>
  }
}

we now deny patterns containing unnecessarily nested html! macros

Patterns like this are now denied with a suggestion to remove the <></>:

html!{
  if foo {
    <>
      <span>{"123"}</span>
      <span>{"234"}</span>
    </>
  }
}

Patterns like this are denied wit a suggestion to remove the inner html!:

html!{
  match foo {
    0 => {
      html!{
        // ...
      }
    }
    // ...
  }
}

Emitting a warning could have been more forgiving. Sadly it's only possible on nightly.

Checklist

  • apply to examples
  • update docs
  • I have reviewed my own code
  • I have added tests

@Madoshakalaka Madoshakalaka added the A-yew-macro Area: The yew-macro crate label Apr 6, 2026
@Madoshakalaka Madoshakalaka changed the title feat(yew-macro): add first-class match and let bindings in for bodies to html! macro feat: add first-class match and let bindings in for bodies to html! macro Apr 6, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

Visit the preview URL for this PR (updated for commit 78dc29c):

https://yew-rs--pr4118-feat-html-match-and-y4khdt3h.web.app

(expires Mon, 13 Apr 2026 09:29:15 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 99.933 99.933 0 0.000%
boids 163.811 163.811 0 0.000%
communication_child_to_parent 93.237 93.237 0 0.000%
communication_grandchild_with_grandparent 105.060 105.060 0 0.000%
communication_grandparent_to_grandchild 101.409 101.409 0 0.000%
communication_parent_to_child 90.646 90.646 0 0.000%
contexts 105.113 105.113 0 0.000%
counter 85.941 85.941 0 0.000%
counter_functional 87.976 87.976 0 0.000%
dyn_create_destroy_apps 89.863 89.863 0 0.000%
file_upload 98.950 98.950 0 0.000%
function_delayed_input 93.929 94.249 +0.320 +0.341%
function_memory_game 169.043 169.207 +0.164 +0.097%
function_router 398.023 398.188 +0.165 +0.041%
function_todomvc 164.074 164.064 -0.010 -0.006%
futures 234.661 234.729 +0.068 +0.029%
game_of_life 100.274 100.354 +0.079 +0.079%
immutable 257.879 257.805 -0.074 -0.029%
inner_html 80.464 80.464 0 0.000%
js_callback 109.085 109.085 0 0.000%
keyed_list 175.767 175.767 0 0.000%
mount_point 83.832 83.832 0 0.000%
nested_list 112.750 112.750 0 0.000%
node_refs 91.222 91.222 0 0.000%
password_strength 1718.437 1718.437 0 0.000%
portals 92.715 92.761 +0.046 +0.050%
router 364.743 364.913 +0.170 +0.047%
suspense 113.063 113.063 0 0.000%
timer 88.079 88.204 +0.125 +0.142%
timer_functional 98.565 98.565 0 0.000%
todomvc 141.760 141.760 0 0.000%
two_apps 85.805 85.805 0 0.000%
web_worker_fib 135.662 135.662 0 0.000%
web_worker_prime 184.043 184.043 0 0.000%
webgl 82.606 82.606 0 0.000%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 302.561 303.533 302.879 0.319
Hello World 10 493.955 505.721 498.427 3.573
Function Router 10 29886.884 31830.343 30764.220 558.637
Concurrent Task 10 1005.445 1008.116 1007.443 0.764
Many Providers 10 1066.670 1137.507 1084.591 21.154

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 302.614 303.542 302.886 0.280
Hello World 10 499.655 518.209 504.963 5.689
Function Router 10 30191.330 31243.970 30675.421 318.066
Concurrent Task 10 1006.686 1008.167 1007.454 0.522
Many Providers 10 1039.441 1086.596 1053.916 13.300

@Madoshakalaka
Copy link
Copy Markdown
Member Author

Madoshakalaka commented Apr 6, 2026

a preliminary application to the examples totals a net reduction of ~60 lines (122 insertions and 184 deletions).

Madoshakalaka and others added 4 commits April 6, 2026 13:33
…ch arms

Allow `=> "text",` and `=> format!(...),` in html! match arms without
requiring braces, matching natural Rust match syntax.
Extend HtmlRootBraced to parse let-stmts and bare literals/expressions,
bringing if/else bodies to parity with match arms and for-loop bodies.
@Madoshakalaka Madoshakalaka changed the title feat: add first-class match and let bindings in for bodies to html! macro feat: first-class match, let bindings, and bare nodes in html! control flow Apr 6, 2026
@Madoshakalaka Madoshakalaka force-pushed the feat/html-match-and-let-in-for branch from 7bd1d40 to 81b0df8 Compare April 6, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-yew-macro Area: The yew-macro crate breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant