From 8cb81d579bc9f537384e321f5101c69921207e62 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Thu, 30 Apr 2026 11:39:06 +0900 Subject: [PATCH 1/2] feat(macro): auto-detect imperative for/while/loop in preamble When a fork-parsed `Stmt::Expr(For/While/Loop, None)` succeeds, the entire expression is Rust-parseable and contains no html elements, so it cannot be a misread of html-control-flow. Accept it as a preamble statement instead of erroring with the `let _ = ...;` hint. Users no longer need a trailing `;` or a `let _ =` binding to use imperative side-effect loops inside an html! body. --- packages/yew-macro/src/html_tree/mod.rs | 60 ++---- .../yew-macro/tests/html_macro/for-pass.rs | 106 +++++++++++ .../html_macro/imperative-preamble-fail.rs | 70 ------- .../imperative-preamble-fail.stderr | 55 ------ .../html_macro/imperative-preamble-pass.rs | 178 ++++++++++++++++++ packages/yew/tests/html_for.rs | 76 ++++++++ website/docs/concepts/html/lists.mdx | 44 +++-- 7 files changed, 401 insertions(+), 188 deletions(-) delete mode 100644 packages/yew-macro/tests/html_macro/imperative-preamble-fail.rs delete mode 100644 packages/yew-macro/tests/html_macro/imperative-preamble-fail.stderr create mode 100644 packages/yew-macro/tests/html_macro/imperative-preamble-pass.rs diff --git a/packages/yew-macro/src/html_tree/mod.rs b/packages/yew-macro/src/html_tree/mod.rs index c2f6e626676..1ee2297c94c 100644 --- a/packages/yew-macro/src/html_tree/mod.rs +++ b/packages/yew-macro/src/html_tree/mod.rs @@ -582,14 +582,17 @@ impl Parse for HtmlRootBraced { /// not Rust-parseable (e.g. an `` or `if cond { }` that the /// Rust expression grammar rejects). /// -/// One pitfall: block-like Rust expressions (`for`, `while`, `loop`, `{...}`) -/// auto-terminate as statements per Rust grammar, so a trailing `;` is not -/// folded into the `Stmt`. Such expressions in preamble position parse as -/// `Stmt::Expr(_, None)` and fall through to the html-control-flow parser. -/// When the entire expression is Rust-parseable (no html elements anywhere -/// inside), the user almost certainly wrote an imperative loop/block, not an -/// html-emitting one - we surface a help message pointing at the -/// `let _ = ...;` workaround. +/// `for`, `while`, and `loop` auto-terminate as statements in Rust grammar, so +/// in preamble position they parse as `Stmt::Expr(_, None)`. When the fork +/// succeeds at parsing one as a full Rust `Stmt`, the entire expression +/// (header, body, all nested calls) was Rust-parseable, which means it +/// contains no html elements and cannot be a misread of html-control-flow. +/// We accept it as a preamble statement so users can write side-effect loops +/// without a trailing `;` or a `let _ =` binding. +/// +/// `if` and `match` are handled separately by the html-control-flow parser +/// because their bodies routinely contain VNode-convertible expressions. +/// Bare `{...}` blocks stay as html-block-as-child (`{ render(item) }`). pub(super) fn parse_preamble_stmts(input: ParseStream) -> syn::Result> { let mut stmts = Vec::new(); loop { @@ -599,21 +602,7 @@ pub(super) fn parse_preamble_stmts(input: ParseStream) -> syn::Result true, Ok(syn::Stmt::Expr(_, Some(_))) => true, Ok(syn::Stmt::Macro(m)) => m.semi_token.is_some(), - Ok(syn::Stmt::Expr(expr, None)) => { - if let Some(kind) = imperative_blocklike_kind(&expr) { - proc_macro_error::emit_error!( - expr, - "this `{}` block is fully Rust-parseable, so it is parsed as html-{} \ - here, but its body cannot produce any html nodes", - kind, kind; - help = "to run a Rust `{}` here for side effects only, bind it with \ - `let _ = ...;` so the parser sees a Rust statement: \ - `let _ = {} ... {{ ... }};`", - kind, kind - ); - } - false - } + Ok(syn::Stmt::Expr(expr, None)) => is_imperative_blocklike(&expr), _ => false, }; if !is_preamble { @@ -625,23 +614,14 @@ pub(super) fn parse_preamble_stmts(input: ParseStream) -> syn::Result Option<&'static str> { - match expr { - syn::Expr::ForLoop(_) => Some("for"), - syn::Expr::While(_) => Some("while"), - syn::Expr::Loop(_) => Some("loop"), - _ => None, - } +/// Whether `expr` is an imperative block-like Rust expression (`for`, `while`, +/// `loop`) that should be accepted as a preamble statement when it appears +/// without a trailing `;`. +fn is_imperative_blocklike(expr: &syn::Expr) -> bool { + matches!( + expr, + syn::Expr::ForLoop(_) | syn::Expr::While(_) | syn::Expr::Loop(_) + ) } /// Parse `break [label] [value]` forgivingly: first try syn's full diff --git a/packages/yew-macro/tests/html_macro/for-pass.rs b/packages/yew-macro/tests/html_macro/for-pass.rs index 4747a2cd8f0..6519fc8b0fb 100644 --- a/packages/yew-macro/tests/html_macro/for-pass.rs +++ b/packages/yew-macro/tests/html_macro/for-pass.rs @@ -390,4 +390,110 @@ fn main() { 0 } let _ = return_value_from_unbraced_arm(); + + // Imperative block-like preambles (`for`, `while`, `loop`, `{...}`) with a + // trailing `;` parse as `Stmt::Expr(_, Some(_))` and are accepted as + // preamble. The bare form (no `;`) is auto-detected by the preamble + // parser; these test that the explicit `;` form keeps working too. + { + let mut sink: ::std::vec::Vec<::std::primitive::i32> = ::std::vec::Vec::new(); + _ = ::yew::html!{ + for x in 0..3_i32 { + let mut acc: ::std::primitive::i32 = 0; + for i in 0..x { + acc += i; + }; + sink.push(acc); + {acc} + } + }; + _ = sink; + } + { + let mut sink: ::std::vec::Vec<::std::primitive::i32> = ::std::vec::Vec::new(); + _ = ::yew::html!{ + for _x in 0..3_i32 { + let mut counter: ::std::primitive::i32 = 0; + while counter < 3 { + counter += 1; + }; + sink.push(counter); + {counter} + } + }; + _ = sink; + } + { + let mut sink: ::std::vec::Vec<::std::primitive::i32> = ::std::vec::Vec::new(); + _ = ::yew::html!{ + for _x in 0..2_i32 { + let mut n: ::std::primitive::i32 = 0; + loop { + n += 1; + if n > 2 { break; } + }; + sink.push(n); + {n} + } + }; + _ = sink; + } + { + let mut sink: ::std::vec::Vec<::std::primitive::i32> = ::std::vec::Vec::new(); + _ = ::yew::html!{ + for x in 0..3_i32 { + { + sink.push(x * 10); + }; + {x} + } + }; + _ = sink; + } + + // Same pattern in a braced match arm preamble. + _ = ::yew::html!{ + match 1_i32 { + 0 => {"zero"}, + _ => { + let mut acc: ::std::primitive::i32 = 0; + for i in 0..3_i32 { + acc += i; + }; + {acc} + } + } + }; + + // Same pattern in a `while` body preamble. + { + let mut counter: ::std::primitive::i32 = 0; + _ = ::yew::html!{ + while counter < 2 { + let mut acc: ::std::primitive::i32 = 0; + for i in 0..counter { + acc += i; + }; + counter += 1; + {acc} + } + }; + _ = counter; + } + + // The `let _ = ...;` form also works, for users who prefer it. + { + let mut sink: ::std::vec::Vec<::std::primitive::i32> = ::std::vec::Vec::new(); + _ = ::yew::html!{ + for x in 0..3_i32 { + let mut acc: ::std::primitive::i32 = 0; + let _ = for i in 0..x { + acc += i; + }; + sink.push(acc); + {acc} + } + }; + _ = sink; + } } diff --git a/packages/yew-macro/tests/html_macro/imperative-preamble-fail.rs b/packages/yew-macro/tests/html_macro/imperative-preamble-fail.rs deleted file mode 100644 index b7c63ab6646..00000000000 --- a/packages/yew-macro/tests/html_macro/imperative-preamble-fail.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Tests for the diagnostic emitted when a block-like Rust expression -// (`for`, `while`, `loop`, `{...}`) appears in preamble position. These -// expressions auto-terminate as statements per Rust grammar, so a trailing -// `;` is not folded into the `Stmt`. The preamble parser rejects them -// (matching `Stmt::Expr(_, None)`) and they would otherwise fall through -// to the html-control-flow parser, where the body's `()` value fails to -// convert to `VNode`. The diagnostic surfaces the pitfall at the right -// span and points at the `let _ = ...;` workaround. - -fn main() { - // Imperative `for` in a `for` body preamble. - _ = ::yew::html! { - for x in 0..3_u32 { - let mut acc: u32 = 0; - for i in 0..x { - acc += i; - } - {acc} - } - }; - - // Imperative `while` in a `for` body preamble. - _ = ::yew::html! { - for _x in 0..3_u32 { - let mut counter: u32 = 0; - while counter < 5 { - counter += 1; - } - {counter} - } - }; - - // Imperative `loop` in a `for` body preamble. - _ = ::yew::html! { - for _x in 0..3_u32 { - let mut n: u32 = 0; - loop { - n += 1; - if n > 3 { break; } - } - {n} - } - }; - - // Imperative `for` in a `while` body preamble. - _ = ::yew::html! { - while false { - let mut acc: u32 = 0; - for i in 0..3_u32 { - acc += i; - } - {acc} - } - }; - - // Imperative `for` in a braced `match` arm preamble. - _ = ::yew::html! { - match 0_u32 { - 0 => { - let mut acc: u32 = 0; - for i in 0..3_u32 { - acc += i; - } - {acc} - } - _ => {"other"}, - } - }; - -} diff --git a/packages/yew-macro/tests/html_macro/imperative-preamble-fail.stderr b/packages/yew-macro/tests/html_macro/imperative-preamble-fail.stderr deleted file mode 100644 index 3b6b2077350..00000000000 --- a/packages/yew-macro/tests/html_macro/imperative-preamble-fail.stderr +++ /dev/null @@ -1,55 +0,0 @@ -error: this `for` block is fully Rust-parseable, so it is parsed as html-for here, but its body cannot produce any html nodes - - = help: to run a Rust `for` here for side effects only, bind it with `let _ = ...;` so the parser sees a Rust statement: `let _ = for ... { ... };` - - --> tests/html_macro/imperative-preamble-fail.rs:15:13 - | -15 | / for i in 0..x { -16 | | acc += i; -17 | | } - | |_____________^ - -error: this `while` block is fully Rust-parseable, so it is parsed as html-while here, but its body cannot produce any html nodes - - = help: to run a Rust `while` here for side effects only, bind it with `let _ = ...;` so the parser sees a Rust statement: `let _ = while ... { ... };` - - --> tests/html_macro/imperative-preamble-fail.rs:26:13 - | -26 | / while counter < 5 { -27 | | counter += 1; -28 | | } - | |_____________^ - -error: this `loop` block is fully Rust-parseable, so it is parsed as html-loop here, but its body cannot produce any html nodes - - = help: to run a Rust `loop` here for side effects only, bind it with `let _ = ...;` so the parser sees a Rust statement: `let _ = loop ... { ... };` - - --> tests/html_macro/imperative-preamble-fail.rs:37:13 - | -37 | / loop { -38 | | n += 1; -39 | | if n > 3 { break; } -40 | | } - | |_____________^ - -error: this `for` block is fully Rust-parseable, so it is parsed as html-for here, but its body cannot produce any html nodes - - = help: to run a Rust `for` here for side effects only, bind it with `let _ = ...;` so the parser sees a Rust statement: `let _ = for ... { ... };` - - --> tests/html_macro/imperative-preamble-fail.rs:49:13 - | -49 | / for i in 0..3_u32 { -50 | | acc += i; -51 | | } - | |_____________^ - -error: this `for` block is fully Rust-parseable, so it is parsed as html-for here, but its body cannot produce any html nodes - - = help: to run a Rust `for` here for side effects only, bind it with `let _ = ...;` so the parser sees a Rust statement: `let _ = for ... { ... };` - - --> tests/html_macro/imperative-preamble-fail.rs:61:17 - | -61 | / for i in 0..3_u32 { -62 | | acc += i; -63 | | } - | |_________________^ diff --git a/packages/yew-macro/tests/html_macro/imperative-preamble-pass.rs b/packages/yew-macro/tests/html_macro/imperative-preamble-pass.rs new file mode 100644 index 00000000000..0eecb66a70e --- /dev/null +++ b/packages/yew-macro/tests/html_macro/imperative-preamble-pass.rs @@ -0,0 +1,178 @@ +// Imperative `for`, `while`, and `loop` blocks in preamble position are +// detected as Rust statements when the entire expression is Rust-parseable +// (no html elements anywhere inside). The user does not need to add a +// trailing `;` or wrap the loop in `let _ = ...;`. + +fn main() { + // Imperative `for` in a `for` body preamble. + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for x in 0..3_u32 { + let mut acc: u32 = 0; + for i in 0..x { + acc += i; + } + tally.push(acc); + {acc} + } + }; + ::std::assert_eq!(tally, ::std::vec![0, 0, 1]); + } + + // Imperative `while` in a `for` body preamble. + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for _x in 0..3_u32 { + let mut counter: u32 = 0; + while counter < 5 { + counter += 1; + } + tally.push(counter); + {counter} + } + }; + ::std::assert_eq!(tally, ::std::vec![5, 5, 5]); + } + + // Imperative `loop` in a `for` body preamble. + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for _x in 0..3_u32 { + let mut n: u32 = 0; + loop { + n += 1; + if n > 3 { break; } + } + tally.push(n); + {n} + } + }; + ::std::assert_eq!(tally, ::std::vec![4, 4, 4]); + } + + // Imperative `for` in a `while` body preamble. + { + let mut counter: u32 = 0; + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + while counter < 3 { + let mut acc: u32 = 0; + for i in 0..3_u32 { + acc += i; + } + tally.push(acc); + counter += 1; + {acc} + } + }; + ::std::assert_eq!(tally, ::std::vec![3, 3, 3]); + } + + // Imperative `for` in a braced `match` arm preamble. + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + match 0_u32 { + 0 => { + let mut acc: u32 = 0; + for i in 0..3_u32 { + acc += i; + } + tally.push(acc); + {acc} + } + _ => {"other"}, + } + }; + ::std::assert_eq!(tally, ::std::vec![3]); + } + + // Labeled imperative `for` (fully Rust-parseable). + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for x in 0..3_u32 { + let mut acc: u32 = 0; + 'inner: for i in 0..10_u32 { + if i > x { break 'inner; } + acc += i; + } + tally.push(acc); + {acc} + } + }; + ::std::assert_eq!(tally, ::std::vec![0, 1, 3]); + } + + // Imperative `for` followed by another preamble statement, then html. + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for x in 0..3_u32 { + let mut acc: u32 = 0; + for i in 0..x { + acc += i; + } + let label = acc * 10; + tally.push(label); + {label} + } + }; + ::std::assert_eq!(tally, ::std::vec![0, 0, 10]); + } + + // Several imperative loops back to back in the same preamble. + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for x in 0..2_u32 { + let mut acc: u32 = 0; + for i in 0..x { + acc += i; + } + let mut bonus: u32 = 0; + while bonus < 2 { + bonus += 1; + } + tally.push(acc + bonus); + {acc + bonus} + } + }; + ::std::assert_eq!(tally, ::std::vec![2, 2]); + } + + // The trailing-`;` form still works (regression). + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for x in 0..3_u32 { + let mut acc: u32 = 0; + for i in 0..x { + acc += i; + }; + tally.push(acc); + {acc} + } + }; + ::std::assert_eq!(tally, ::std::vec![0, 0, 1]); + } + + // The `let _ = ...;` form still works (regression). + { + let mut tally: ::std::vec::Vec = ::std::vec::Vec::new(); + _ = ::yew::html! { + for x in 0..3_u32 { + let mut acc: u32 = 0; + let _ = for i in 0..x { + acc += i; + }; + tally.push(acc); + {acc} + } + }; + ::std::assert_eq!(tally, ::std::vec![0, 0, 1]); + } +} diff --git a/packages/yew/tests/html_for.rs b/packages/yew/tests/html_for.rs index 6b6309437c0..66cea198e1b 100644 --- a/packages/yew/tests/html_for.rs +++ b/packages/yew/tests/html_for.rs @@ -343,3 +343,79 @@ async fn for_break_workaround_with_braced_arm() { "012" ); } + +#[wasm_bindgen_test] +async fn for_imperative_inner_for_runs_as_preamble() { + // A bare imperative `for` (no trailing `;`) inside an html-`for` body + // is auto-detected as a preamble Rust statement, not html-control-flow. + // Each outer iteration runs the inner side-effect loop and then emits + // a single `` with the accumulated value. + #[component] + fn App() -> Html { + html! { +
+ for x in 0..3_u32 { + let mut acc: u32 = 0; + for i in 0..=x { + acc += i; + } + {acc} + } +
+ } + } + + assert_eq!( + render_and_read::().await, + "013" + ); +} + +#[wasm_bindgen_test] +async fn for_imperative_inner_while_runs_as_preamble() { + #[component] + fn App() -> Html { + html! { +
+ for _x in 0..2_u32 { + let mut counter: u32 = 0; + while counter < 3 { + counter += 1; + } + {counter} + } +
+ } + } + + assert_eq!( + render_and_read::().await, + "33" + ); +} + +#[wasm_bindgen_test] +async fn for_imperative_inner_loop_runs_as_preamble() { + #[component] + fn App() -> Html { + html! { +
+ for x in 1..=3_u32 { + let mut n: u32 = 0; + loop { + n += 1; + if n >= x { + break; + } + } + {n} + } +
+ } + } + + assert_eq!( + render_and_read::().await, + "123" + ); +} diff --git a/website/docs/concepts/html/lists.mdx b/website/docs/concepts/html/lists.mdx index 7111aa90a9e..82987e79d18 100644 --- a/website/docs/concepts/html/lists.mdx +++ b/website/docs/concepts/html/lists.mdx @@ -25,10 +25,10 @@ html! { }; ``` -`for` loop bodies accept Rust statements before the html children. Any -terminated statement works: `let` bindings, expression statements ending in -`;`, item definitions (`fn`, `struct`, `use`, ...), and macro invocations with -`;`. +`for` loop bodies accept Rust statements before the html children: `let` +bindings, expression statements ending in `;`, item definitions (`fn`, +`struct`, `use`, ...), macro invocations with `;`, and imperative `for`, +`while`, or `loop` blocks for side effects. ```rust , ignore use yew::prelude::*; @@ -37,7 +37,12 @@ html! { for item in items { let label = format!("{}: {}", item.id, item.name); let class = if item.active { "active" } else { "inactive" }; -
{label}
+ let mut tags = String::new(); + for tag in &item.tags { + tags.push_str(tag); + tags.push(' '); + } +
{label}
} }; ``` @@ -112,15 +117,15 @@ A bare qualified-path expression like `::method(...)` collides with the :::note Imperative loops in the preamble -Block-like Rust expressions (`for`, `while`, `loop`, `{...}`) auto-terminate as -statements in Rust grammar - a trailing `;` is not folded into the statement. -A bare imperative loop in a preamble: +A nested `for`, `while`, or `loop` whose body has only Rust statements (no +html elements anywhere inside) is detected as a Rust statement, not as +html-control-flow. So this works as written: ```rust , ignore html! { for item in items.iter() { let mut by_han = BTreeMap::new(); - for src in &item.sources { // imperative side-effect loop + for src in &item.sources { by_han.entry(src.han_nom.clone()).or_default().push(src); }
{render(by_han)}
@@ -128,34 +133,27 @@ html! { } ``` -is parsed as html-`for` (emitting children) instead of as a Rust statement, -and its body's `()` value cannot be converted to a `VNode`. Bind it with -`let _ = ...;` so the parser sees a `Stmt::Local`: +If you mix html into the inner loop, the macro takes that loop as +html-control-flow instead and emits its children per iteration: ```rust , ignore html! { for item in items.iter() { - let mut by_han = BTreeMap::new(); - let _ = for src in &item.sources { - by_han.entry(src.han_nom.clone()).or_default().push(src); - }; -
{render(by_han)}
+ for tag in &item.tags { + {tag} + } } } ``` -The same applies to imperative `while`, `loop`, and bare-block `{...}` -statements. `match` and `if` are not subject to this collision because their -html-control-flow forms naturally accept the same body shapes as their -imperative use. - ::: `while` and `while let` loops work the same way as `for`, producing a list of nodes from their body. All the statement forms accepted by `for` bodies (let bindings, expression statements, -items, macros) are also accepted here, along with `if` blocks and `break`/`continue`. +items, macros, and imperative `for`/`while`/`loop` blocks) are also accepted here, along with +`if` blocks and `break`/`continue`. ```rust use yew::prelude::*; From e611fb0d5c90582ec76b28704ec2a83b4cd46881 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Thu, 30 Apr 2026 17:05:48 +0900 Subject: [PATCH 2/2] website: add `if { for {x} }` workarounds --- .../concepts/html/conditional-rendering.mdx | 43 +++++++++++++ website/docs/concepts/html/lists.mdx | 64 +++++++++++++++++++ .../concepts/html/conditional-rendering.mdx | 39 +++++++++++ .../current/concepts/html/lists.mdx | 57 +++++++++++++++++ .../concepts/html/conditional-rendering.mdx | 39 +++++++++++ .../current/concepts/html/lists.mdx | 57 +++++++++++++++++ .../concepts/html/conditional-rendering.mdx | 39 +++++++++++ .../current/concepts/html/lists.mdx | 57 +++++++++++++++++ 8 files changed, 395 insertions(+) diff --git a/website/docs/concepts/html/conditional-rendering.mdx b/website/docs/concepts/html/conditional-rendering.mdx index f1f935a6347..422a92374af 100644 --- a/website/docs/concepts/html/conditional-rendering.mdx +++ b/website/docs/concepts/html/conditional-rendering.mdx @@ -156,3 +156,46 @@ html! { Arms with a single element can omit braces. Arms with multiple children or `let` bindings require braces. `match` supports all standard Rust patterns including OR-patterns (`A | B`), destructuring, and `if` guards. Exhaustiveness is checked by the Rust compiler. + +## Loops inside `if`/`match` bodies + +`for`, `while`, and `loop` inside an `if`/`else if`/`else`/`match`-arm body +follow the same rules they do at the top level of `html!`, with one caveat: +if the loop body is fully Rust-parseable (no html elements anywhere inside), +the macro takes the loop as a Rust statement instead of html-control-flow. + +The most common way to hit this is a loop whose only child is a bare +`{expr}` block: + +```rust , ignore +// Compiles. The for is at the top level, parsed as html-`for`. +html! { + for _ in 0..9 { + {my_foo} + } +} + +// Does NOT compile. The for is inside an `if`, parsed as a Rust statement; +// rustc then complains that the body returns a value where `()` is expected. +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +Wrap the child in an html element to keep it as html-`for`: + +```rust , ignore +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +See [Lists](./lists.mdx) for the full set of workarounds. diff --git a/website/docs/concepts/html/lists.mdx b/website/docs/concepts/html/lists.mdx index 82987e79d18..8628c14c672 100644 --- a/website/docs/concepts/html/lists.mdx +++ b/website/docs/concepts/html/lists.mdx @@ -146,6 +146,70 @@ html! { } ``` +::: + +:::caution `for` with a bare `{expr}` body inside `if`/`match`/`while`/another `for` + +A `for` whose only child is a bare `{expr}` block renders one node per +iteration at the **top level** of an `html!`, but the same code nested +inside an `if`/`match` arm/`while`/another `for` body is rejected with a +`mismatched types: expected (), found T` error. The two positions use +different parsing strategies, and the nested one tries to parse the loop +as a Rust statement first. If the body is fully Rust-parseable (which a +bare `{expr}` block is), it stops being html-control-flow. + +```rust , ignore +// Works: the for is at the top level. +html! { + for _ in 0..9 { + {my_foo} + } +} + +// Does NOT compile: the for is nested inside an `if`. +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +To render `my_foo` 9 times conditionally, pick one of: + +```rust , ignore +// 1. Wrap the child in an html element (recommended). +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} + +// 2. Push the for to the top level of a nested `html!`. +html! { + if condition { + { html! { + for _ in 0..9 { + {my_foo} + } + } } + } +} + +// 3. Build the list with an iterator and `collect::()`. +html! { + if condition { + { (0..9).map(|_| html!({my_foo})).collect::() } + } +} +``` + +The same rule applies inside `match` arms, `while` bodies, and the body +of an outer `for`. + ::: diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx index 0d5c3e726ad..90af1f56580 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx @@ -156,3 +156,42 @@ html! { 単一要素のアームはブレースを省略できます。複数の子要素や `let` バインディングを持つアームにはブレースが必要です。 `match` は OR パターン(`A | B`)、分割代入、`if` ガードを含む、すべての標準的な Rust パターンをサポートします。網羅性は Rust コンパイラによってチェックされます。 + +## `if` / `match` の本体内部のループ + +`if` / `else if` / `else` / `match` のアーム本体内部の `for`、`while`、`loop` は、`html!` のトップレベル位置と同じ規則に従いますが、一つだけ例外があります:ループ本体が完全に Rust として解析可能な場合(内部のどこにも html 要素がない場合)、マクロはそのループを html の制御フローではなく Rust の文として扱います。 + +もっとも遭遇しやすいのは、ループ本体に裸の `{expr}` ブロックしか書かれていないケースです: + +```rust , ignore +// コンパイル可能。for はトップレベルにあり、html-`for` として解析されます。 +html! { + for _ in 0..9 { + {my_foo} + } +} + +// コンパイル不可。for は `if` の内部にあり、Rust の文として解析されます。 +// その後 rustc が、ループ本体の戻り型が `()` ではないと報告します。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +子要素を html 要素で囲めば html-`for` の挙動が保たれます: + +```rust , ignore +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +回避策の全体像は[リスト](./lists.mdx)を参照してください。 diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx index fae271b25c4..09843bec909 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx @@ -97,6 +97,63 @@ let mut rendered = Vec::new(); - 末尾に `;` を付けてプリアンブルの式文にします:`::method(...);`。戻り値は破棄されます。 - `{...}` で囲んで戻り値をノードとして使います:`{ ::method(...) }`。 +::: + +:::caution `if` / `match` / `while` / 外側の `for` の内部で本体が裸の `{expr}` だけの `for` + +`for` ループの本体に裸の `{expr}` ブロックしか書かれていない場合、`html!` の**トップレベル**にあれば反復ごとに 1 ノードずつ描画されます。ところが同じコードを `if` / `match` のアーム / `while` / 外側の `for` の本体に入れ子で置くと、`mismatched types: expected (), found T` というエラーで拒否されます。トップレベルと入れ子の位置とでは解析方針が異なり、入れ子の位置ではループをまず Rust の文として解析しようとします。本体が完全に Rust として解析可能であれば(裸の `{expr}` ブロックはまさにそうです)、html の制御フローとして扱われなくなります。 + +```rust , ignore +// コンパイル可能:for はトップレベルにあります。 +html! { + for _ in 0..9 { + {my_foo} + } +} + +// コンパイル不可:for は `if` の内部に入れ子になっています。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +`my_foo` を条件付きで 9 回描画したい場合は、次のいずれかを選んでください: + +```rust , ignore +// 1. 子要素を html 要素で囲む(推奨)。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} + +// 2. for をネストした `html!` のトップレベルへ移す。 +html! { + if condition { + { html! { + for _ in 0..9 { + {my_foo} + } + } } + } +} + +// 3. イテレータと `collect::()` でリストを組み立てる。 +html! { + if condition { + { (0..9).map(|_| html!({my_foo})).collect::() } + } +} +``` + +同じ規則は `match` のアーム本体、`while` の本体、外側の `for` の本体にも当てはまります。 + ::: diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx index 1dd65ef3665..f1b58782fb6 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx @@ -156,3 +156,42 @@ html! { 单元素 arm 可以省略大括号。含多个子节点或 `let` 绑定的 arm 需要大括号。 `match` 支持所有标准 Rust 模式,包括 OR 模式(`A | B`)、解构和 `if` 守卫。穷尽性由 Rust 编译器检查。 + +## `if` / `match` 体内部的循环 + +`if` / `else if` / `else` / `match` 分支体内部的 `for`、`while`、`loop` 与 `html!` 顶层位置的规则相同,但有一个例外:如果循环体可以被 Rust 完整解析(内部任何位置都没有 html 元素),宏会把这个循环视为 Rust 语句而不是 html 控制流。 + +最常见的触发情形是循环体里只有一个裸 `{expr}` 块: + +```rust , ignore +// 可以编译。for 在最外层,被解析为 html-`for`。 +html! { + for _ in 0..9 { + {my_foo} + } +} + +// 无法编译。for 在 `if` 内部,被解析为 Rust 语句; +// 之后 rustc 会报告循环体返回的类型不是 `()`。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +把子节点用 html 元素包起来即可保留 html-`for` 行为: + +```rust , ignore +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +完整的解决方案集合请参见[列表](./lists.mdx)。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx index 35a17177329..8b6e5837a5e 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx @@ -97,6 +97,63 @@ let mut rendered = Vec::new(); - 在末尾加上 `;`,使其成为前置的表达式语句:`::method(...);`。返回值会被丢弃。 - 用 `{...}` 包起来,将返回值作为节点使用:`{ ::method(...) }`。 +::: + +:::caution 在 `if` / `match` / `while` / 外层 `for` 内部使用 `{expr}` 作为 `for` 循环体 + +如果 `for` 循环体只包含一个裸 `{expr}` 块,在 `html!` 的**最外层**位置可以正常工作,每次迭代渲染一个节点;但同样的代码嵌套在 `if` / `match` 分支 / `while` / 另一个 `for` 体内时会被拒绝,并报告 `mismatched types: expected (), found T`。这两个位置使用的解析策略不同:嵌套位置会先尝试把循环解析为 Rust 语句。如果循环体能完全被 Rust 解析(裸 `{expr}` 块就是这种情况),它就不再被视为 html 控制流。 + +```rust , ignore +// 可以编译:for 位于最外层。 +html! { + for _ in 0..9 { + {my_foo} + } +} + +// 无法编译:for 嵌套在 `if` 内部。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +要在条件成立时把 `my_foo` 渲染 9 次,请选择以下任一方式: + +```rust , ignore +// 1. 用 html 元素包裹子节点(推荐)。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} + +// 2. 把 for 放进嵌套的 `html!` 顶层。 +html! { + if condition { + { html! { + for _ in 0..9 { + {my_foo} + } + } } + } +} + +// 3. 用迭代器配合 `collect::()` 构建列表。 +html! { + if condition { + { (0..9).map(|_| html!({my_foo})).collect::() } + } +} +``` + +同样的规则也适用于 `match` 分支、`while` 循环体以及外层 `for` 的循环体。 + ::: diff --git a/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx b/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx index b758ffd7b09..8f52272bd63 100644 --- a/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx +++ b/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/conditional-rendering.mdx @@ -156,3 +156,42 @@ html! { 單元素 arm 可以省略大括號。含多個子節點或 `let` 綁定的 arm 需要大括號。 `match` 支援所有標準 Rust 模式,包括 OR 模式(`A | B`)、解構和 `if` 守衛。窮舉性由 Rust 編譯器檢查。 + +## `if` / `match` 主體內部的迴圈 + +`if` / `else if` / `else` / `match` 分支主體內部的 `for`、`while`、`loop` 與 `html!` 最外層位置的規則相同,但有一個例外:如果迴圈主體可以被 Rust 完整剖析(內部任何位置都沒有 html 元素),巨集會把這個迴圈視為 Rust 陳述式而不是 html 控制流。 + +最常見的觸發情形是迴圈主體裡只有一個裸 `{expr}` 區塊: + +```rust , ignore +// 可以編譯。for 在最外層,被解析為 html-`for`。 +html! { + for _ in 0..9 { + {my_foo} + } +} + +// 無法編譯。for 在 `if` 內部,被解析為 Rust 陳述式; +// 之後 rustc 會回報迴圈主體傳回的型別不是 `()`。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +把子節點用 html 元素包起來即可保留 html-`for` 行為: + +```rust , ignore +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +完整的解決方法集合請參見[清單](./lists.mdx)。 diff --git a/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx b/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx index 8bde1886279..c74cc8cc292 100644 --- a/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx +++ b/website/i18n/zh-Hant/docusaurus-plugin-content-docs/current/concepts/html/lists.mdx @@ -97,6 +97,63 @@ let mut rendered = Vec::new(); - 在結尾加上 `;`,讓它成為前置的運算式陳述式:`::method(...);`。傳回值會被丟棄。 - 用 `{...}` 包起來,將傳回值當作節點使用:`{ ::method(...) }`。 +::: + +:::caution 在 `if` / `match` / `while` / 外層 `for` 內部使用 `{expr}` 作為 `for` 迴圈主體 + +如果 `for` 迴圈主體只包含一個裸 `{expr}` 區塊,在 `html!` 的**最外層**位置可以正常運作,每次迭代會渲染一個節點;但同樣的程式碼嵌套在 `if` / `match` 分支 / `while` / 另一個 `for` 主體內時會被拒絕,並回報 `mismatched types: expected (), found T`。這兩個位置使用不同的剖析策略:嵌套位置會先嘗試將迴圈解析為 Rust 陳述式。如果迴圈主體能完全被 Rust 剖析(裸 `{expr}` 區塊就是這種情況),它就不再被視為 html 控制流。 + +```rust , ignore +// 可以編譯:for 位於最外層。 +html! { + for _ in 0..9 { + {my_foo} + } +} + +// 無法編譯:for 嵌套在 `if` 內部。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} +``` + +要在條件成立時把 `my_foo` 渲染 9 次,請選擇以下任一方式: + +```rust , ignore +// 1. 用 html 元素包裹子節點(建議做法)。 +html! { + if condition { + for _ in 0..9 { + {my_foo} + } + } +} + +// 2. 把 for 放進巢狀 `html!` 的最外層。 +html! { + if condition { + { html! { + for _ in 0..9 { + {my_foo} + } + } } + } +} + +// 3. 使用迭代器搭配 `collect::()` 建構清單。 +html! { + if condition { + { (0..9).map(|_| html!({my_foo})).collect::() } + } +} +``` + +同樣的規則也適用於 `match` 分支、`while` 迴圈主體以及外層 `for` 的迴圈主體。 + :::