Add for-loops to html! (#3498)

This commit is contained in:
Tim Kurdov 2025-07-15 06:29:27 +00:00 committed by GitHub
parent 8945ab7856
commit fd96a9abca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 504 additions and 156 deletions

View File

@ -84,7 +84,11 @@ pub fn RenderLinks(props: &RenderLinksProps) -> Html {
</>
}
} else {
html! { for range.map(|page| html! {<RenderLink to_page={page} props={props.clone()} />}) }
html! {
for page in range {
<RenderLink to_page={page} props={props.clone()} />
}
}
}
}

View File

@ -118,7 +118,7 @@ pub fn Post(props: &Props) -> Html {
render_quote(quote)
}
});
html! { for parts }
html! {{for parts}}
};
let keywords = post

View File

@ -80,7 +80,11 @@ impl Pagination {
</>
}
} else {
html! { for pages.map(|page| self.render_link(page, props)) }
html! {
for page in pages {
{self.render_link(page, props)}
}
}
}
}

View File

@ -137,6 +137,6 @@ impl Post {
self.render_quote(quote)
}
});
html! { for parts }
html! {{for parts}}
}
}

View File

@ -59,4 +59,11 @@ impl ToNodeIterator for HtmlBlock {
Some(quote_spanned! {brace.span=> #new_tokens})
}
fn is_singular(&self) -> bool {
match &self.content {
BlockContent::Node(node) => node.is_singular(),
BlockContent::Iterable(_) => false,
}
}
}

View File

@ -11,7 +11,7 @@ use crate::props::ComponentProps;
pub struct HtmlComponent {
ty: Type,
props: ComponentProps,
pub props: ComponentProps,
children: HtmlChildrenTree,
close: Option<HtmlComponentClose>,
}

View File

@ -0,0 +1,115 @@
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::token::{For, In};
use syn::{braced, Expr, Pat};
use super::{HtmlChildrenTree, ToNodeIterator};
use crate::html_tree::HtmlTree;
use crate::PeekValue;
/// Determines if an expression is guaranteed to always return the same value anywhere.
fn is_contextless_pure(expr: &Expr) -> bool {
match expr {
Expr::Lit(_) => true,
Expr::Path(path) => path.path.get_ident().is_none(),
_ => false,
}
}
pub struct HtmlFor {
pat: Pat,
iter: Expr,
body: HtmlChildrenTree,
}
impl PeekValue<()> for HtmlFor {
fn peek(cursor: Cursor) -> Option<()> {
let (ident, _) = cursor.ident()?;
(ident == "for").then_some(())
}
}
impl Parse for HtmlFor {
fn parse(input: ParseStream) -> syn::Result<Self> {
For::parse(input)?;
let pat = Pat::parse_single(input)?;
In::parse(input)?;
let iter = Expr::parse_without_eager_brace(input)?;
let body_stream;
braced!(body_stream in input);
let body = HtmlChildrenTree::parse_delimited(&body_stream)?;
// TODO: more concise code by using if-let guards once MSRV is raised
for child in body.0.iter() {
let HtmlTree::Element(element) = child else {
continue;
};
let Some(key) = &element.props.special.key else {
continue;
};
if is_contextless_pure(&key.value) {
return Err(syn::Error::new(
key.value.span(),
"duplicate key for a node in a `for`-loop\nthis will create elements with \
duplicate keys if the loop iterates more than once",
));
}
}
Ok(Self { pat, iter, body })
}
}
impl ToTokens for HtmlFor {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self { pat, iter, body } = self;
let acc = Ident::new("__yew_v", iter.span());
let alloc_opt = body
.size_hint()
.filter(|&size| size > 1) // explicitly reserving space for 1 more element is redundant
.map(|size| quote!( #acc.reserve(#size) ));
let vlist_gen = match body.fully_keyed() {
Some(true) => quote! {
::yew::virtual_dom::VList::__macro_new(
#acc,
::std::option::Option::None,
::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed
)
},
Some(false) => quote! {
::yew::virtual_dom::VList::__macro_new(
#acc,
::std::option::Option::None,
::yew::virtual_dom::FullyKeyedState::KnownMissingKeys
)
},
None => quote! {
::yew::virtual_dom::VList::with_children(#acc, ::std::option::Option::None)
},
};
let body = body.0.iter().map(|child| {
if let Some(child) = child.to_node_iterator_stream() {
quote!( #acc.extend(#child) )
} else {
quote!( #acc.push(::std::convert::Into::into(#child)) )
}
});
tokens.extend(quote!({
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
::std::iter::Iterator::for_each(
::std::iter::IntoIterator::into_iter(#iter),
|#pat| { #alloc_opt; #(#body);* }
);
#vlist_gen
}))
}
}

View File

@ -3,9 +3,9 @@ use quote::{quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{Expr, Token};
use syn::{parse_quote, Expr, Token};
use super::{HtmlRootBraced, ToNodeIterator};
use super::HtmlRootBraced;
use crate::PeekValue;
pub struct HtmlIf {
@ -69,13 +69,13 @@ impl Parse for HtmlIf {
impl ToTokens for HtmlIf {
fn to_tokens(&self, tokens: &mut TokenStream) {
let HtmlIf {
let Self {
if_token,
cond,
then_branch,
else_branch,
} = self;
let default_else_branch = syn::parse_quote! { {} };
let default_else_branch = parse_quote! { {} };
let else_branch = else_branch
.as_ref()
.map(|(_, branch)| branch)
@ -88,27 +88,6 @@ impl ToTokens for HtmlIf {
}
}
impl ToNodeIterator for HtmlIf {
fn to_node_iterator_stream(&self) -> Option<TokenStream> {
let HtmlIf {
if_token,
cond,
then_branch,
else_branch,
} = self;
let default_else_branch = syn::parse_str("{}").unwrap();
let else_branch = else_branch
.as_ref()
.map(|(_, branch)| branch)
.unwrap_or(&default_else_branch);
let new_tokens = quote_spanned! {if_token.span()=>
if #cond #then_branch else #else_branch
};
Some(quote_spanned! {if_token.span=> #new_tokens})
}
}
pub enum HtmlRootBracedOrIf {
Branch(HtmlRootBraced),
If(HtmlIf),

View File

@ -58,4 +58,8 @@ impl ToNodeIterator for HtmlIterable {
::yew::utils::into_node_iter(#expr)
})
}
fn is_singular(&self) -> bool {
false
}
}

View File

@ -10,7 +10,7 @@ use crate::props::Prop;
use crate::{Peek, PeekValue};
pub struct HtmlList {
open: HtmlListOpen,
pub open: HtmlListOpen,
pub children: HtmlChildrenTree,
close: HtmlListClose,
}
@ -71,23 +71,30 @@ impl ToTokens for HtmlList {
quote! { ::std::option::Option::None }
};
let spanned = {
let span = {
let open = open.to_spanned();
let close = close.to_spanned();
quote! { #open #close }
};
}
.span();
tokens.extend(quote_spanned! {spanned.span()=>
::yew::virtual_dom::VNode::VList(::std::rc::Rc::new(
tokens.extend(match children.fully_keyed() {
Some(true) => quote_spanned!{span=>
::yew::virtual_dom::VList::__macro_new(#children, #key, ::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed)
},
Some(false) => quote_spanned!{span=>
::yew::virtual_dom::VList::__macro_new(#children, #key, ::yew::virtual_dom::FullyKeyedState::KnownMissingKeys)
},
None => quote_spanned!{span=>
::yew::virtual_dom::VList::with_children(#children, #key)
))
}
});
}
}
struct HtmlListOpen {
pub struct HtmlListOpen {
tag: TagTokens,
props: HtmlListProps,
pub props: HtmlListProps,
}
impl HtmlListOpen {
fn to_spanned(&self) -> impl ToTokens {
@ -121,8 +128,8 @@ impl Parse for HtmlListOpen {
}
}
struct HtmlListProps {
key: Option<Expr>,
pub struct HtmlListProps {
pub key: Option<Expr>,
}
impl Parse for HtmlListProps {
fn parse(input: ParseStream) -> syn::Result<Self> {

View File

@ -67,8 +67,8 @@ impl ToTokens for HtmlNode {
impl ToNodeIterator for HtmlNode {
fn to_node_iterator_stream(&self) -> Option<TokenStream> {
match self {
HtmlNode::Literal(_) => None,
HtmlNode::Expression(expr) => {
Self::Literal(_) => None,
Self::Expression(expr) => {
// NodeSeq turns both Into<T> and Vec<Into<T>> into IntoIterator<Item = T>
Some(quote_spanned! {expr.span().resolved_at(Span::call_site())=>
::std::convert::Into::<::yew::utils::NodeSeq<_, _>>::into(#expr)
@ -76,4 +76,11 @@ impl ToNodeIterator for HtmlNode {
}
}
}
fn is_singular(&self) -> bool {
match self {
Self::Literal(_) => true,
Self::Expression(_) => false,
}
}
}

View File

@ -12,6 +12,7 @@ mod html_block;
mod html_component;
mod html_dashed_name;
mod html_element;
mod html_for;
mod html_if;
mod html_iterable;
mod html_list;
@ -30,6 +31,7 @@ use html_node::HtmlNode;
use tag::TagTokens;
use self::html_block::BlockContent;
use self::html_for::HtmlFor;
pub enum HtmlType {
Block,
@ -37,6 +39,7 @@ pub enum HtmlType {
List,
Element,
If,
For,
Empty,
}
@ -46,6 +49,7 @@ pub enum HtmlTree {
List(Box<HtmlList>),
Element(Box<HtmlElement>),
If(Box<HtmlIf>),
For(Box<HtmlFor>),
Empty,
}
@ -53,15 +57,15 @@ impl Parse for HtmlTree {
fn parse(input: ParseStream) -> syn::Result<Self> {
let html_type = Self::peek_html_type(input)
.ok_or_else(|| input.error("expected a valid html element"))?;
let html_tree = match html_type {
HtmlType::Empty => HtmlTree::Empty,
HtmlType::Component => HtmlTree::Component(Box::new(input.parse()?)),
HtmlType::Element => HtmlTree::Element(Box::new(input.parse()?)),
HtmlType::Block => HtmlTree::Block(Box::new(input.parse()?)),
HtmlType::List => HtmlTree::List(Box::new(input.parse()?)),
HtmlType::If => HtmlTree::If(Box::new(input.parse()?)),
};
Ok(html_tree)
Ok(match html_type {
HtmlType::Empty => Self::Empty,
HtmlType::Component => Self::Component(Box::new(input.parse()?)),
HtmlType::Element => Self::Element(Box::new(input.parse()?)),
HtmlType::Block => Self::Block(Box::new(input.parse()?)),
HtmlType::List => Self::List(Box::new(input.parse()?)),
HtmlType::If => Self::If(Box::new(input.parse()?)),
HtmlType::For => Self::For(Box::new(input.parse()?)),
})
}
}
@ -72,17 +76,16 @@ impl HtmlTree {
/// returns with the appropriate type. If invalid html tag, returns `None`.
fn peek_html_type(input: ParseStream) -> Option<HtmlType> {
let input = input.fork(); // do not modify original ParseStream
let cursor = input.cursor();
if input.is_empty() {
Some(HtmlType::Empty)
} else if input
.cursor()
.group(proc_macro2::Delimiter::Brace)
.is_some()
{
} else if HtmlBlock::peek(cursor).is_some() {
Some(HtmlType::Block)
} else if HtmlIf::peek(input.cursor()).is_some() {
} else if HtmlIf::peek(cursor).is_some() {
Some(HtmlType::If)
} else if HtmlFor::peek(cursor).is_some() {
Some(HtmlType::For)
} else if input.peek(Token![<]) {
let _lt: Token![<] = input.parse().ok()?;
@ -122,21 +125,21 @@ impl ToTokens for HtmlTree {
fn to_tokens(&self, tokens: &mut TokenStream) {
lint::lint_all(self);
match self {
HtmlTree::Empty => tokens.extend(quote! {
Self::Empty => tokens.extend(quote! {
<::yew::virtual_dom::VNode as ::std::default::Default>::default()
}),
HtmlTree::Component(comp) => comp.to_tokens(tokens),
HtmlTree::Element(tag) => tag.to_tokens(tokens),
HtmlTree::List(list) => list.to_tokens(tokens),
HtmlTree::Block(block) => block.to_tokens(tokens),
HtmlTree::If(block) => block.to_tokens(tokens),
Self::Component(comp) => comp.to_tokens(tokens),
Self::Element(tag) => tag.to_tokens(tokens),
Self::List(list) => list.to_tokens(tokens),
Self::Block(block) => block.to_tokens(tokens),
Self::If(block) => block.to_tokens(tokens),
Self::For(block) => block.to_tokens(tokens),
}
}
}
pub enum HtmlRoot {
Tree(HtmlTree),
Iterable(Box<HtmlIterable>),
Node(Box<HtmlNode>),
}
@ -144,8 +147,6 @@ impl Parse for HtmlRoot {
fn parse(input: ParseStream) -> syn::Result<Self> {
let html_root = if HtmlTree::peek_html_type(input).is_some() {
Self::Tree(input.parse()?)
} else if HtmlIterable::peek(input.cursor()).is_some() {
Self::Iterable(Box::new(input.parse()?))
} else {
Self::Node(Box::new(input.parse()?))
};
@ -168,7 +169,6 @@ impl ToTokens for HtmlRoot {
match self {
Self::Tree(tree) => tree.to_tokens(tokens),
Self::Node(node) => node.to_tokens(tokens),
Self::Iterable(iterable) => iterable.to_tokens(tokens),
}
}
}
@ -200,16 +200,27 @@ pub trait ToNodeIterator {
/// each element. If the resulting iterator only ever yields a single item this function
/// should return None instead.
fn to_node_iterator_stream(&self) -> Option<TokenStream>;
/// Returns a boolean indicating whether the node can only ever unfold into 1 node
/// Same as calling `.to_node_iterator_stream().is_none()`,
/// but doesn't actually construct any token stream
fn is_singular(&self) -> bool;
}
impl ToNodeIterator for HtmlTree {
fn to_node_iterator_stream(&self) -> Option<TokenStream> {
match self {
HtmlTree::Block(block) => block.to_node_iterator_stream(),
Self::Block(block) => block.to_node_iterator_stream(),
// everything else is just a single node.
_ => None,
}
}
fn is_singular(&self) -> bool {
match self {
Self::Block(block) => block.is_singular(),
_ => true,
}
}
}
pub struct HtmlChildrenTree(pub Vec<HtmlTree>);
@ -231,10 +242,7 @@ impl HtmlChildrenTree {
// Check if each child represents a single node.
// This is the case when no expressions are used.
fn only_single_node_children(&self) -> bool {
self.0
.iter()
.map(ToNodeIterator::to_node_iterator_stream)
.all(|s| s.is_none())
self.0.iter().all(HtmlTree::is_singular)
}
pub fn to_build_vec_token_stream(&self) -> TokenStream {
@ -340,6 +348,41 @@ impl HtmlChildrenTree {
},
}
}
pub fn size_hint(&self) -> Option<usize> {
self.only_single_node_children().then_some(self.0.len())
}
pub fn fully_keyed(&self) -> Option<bool> {
for child in self.0.iter() {
match child {
HtmlTree::Block(block) => {
return if let BlockContent::Node(node) = &block.content {
matches!(&**node, HtmlNode::Literal(_)).then_some(false)
} else {
None
}
}
HtmlTree::Component(comp) => {
if comp.props.props.special.key.is_none() {
return Some(false);
}
}
HtmlTree::List(list) => {
if list.open.props.key.is_none() {
return Some(false);
}
}
HtmlTree::Element(element) => {
if element.props.special.key.is_none() {
return Some(false);
}
}
HtmlTree::If(_) | HtmlTree::For(_) | HtmlTree::Empty => return Some(false),
}
}
Some(true)
}
}
impl ToTokens for HtmlChildrenTree {

View File

@ -35,7 +35,7 @@ impl ToTokens for BaseExpr {
}
pub struct ComponentProps {
props: Props,
pub props: Props,
base_expr: Option<Expr>,
}
impl ComponentProps {

View File

@ -688,13 +688,13 @@ note: required by a bound in `ChildContainerPropertiesBuilder::children`
| -------- required by a bound in this associated function
= note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `VChild<Child>: From<VNode>` is not satisfied
error[E0277]: the trait bound `VChild<Child>: From<yew::virtual_dom::VList>` is not satisfied
--> tests/html_macro/component-fail.rs:118:29
|
118 | html! { <ChildContainer><></></ChildContainer> };
| ^ the trait `From<VNode>` is not implemented for `VChild<Child>`, which is required by `VNode: Into<_>`
| ^ the trait `From<yew::virtual_dom::VList>` is not implemented for `VChild<Child>`, which is required by `yew::virtual_dom::VList: Into<_>`
|
= note: required for `VNode` to implement `Into<VChild<Child>>`
= note: required for `yew::virtual_dom::VList` to implement `Into<VChild<Child>>`
error[E0277]: the trait bound `VNode: IntoPropValue<ChildrenRenderer<VChild<Child>>>` is not satisfied
--> tests/html_macro/component-fail.rs:119:30

View File

@ -0,0 +1,24 @@
mod smth {
const KEY: u32 = 42;
}
fn main() {
_ = ::yew::html!{for x};
_ = ::yew::html!{for x in};
_ = ::yew::html!{for x in 0 .. 10};
_ = ::yew::html!{for (x, y) in 0 .. 10 {
<span>{x}</span>
}};
_ = ::yew::html!{for _ in 0 .. 10 {
<span>{break}</span>
}};
_ = ::yew::html!{for _ in 0 .. 10 {
<div key="duplicate" />
}};
_ = ::yew::html!{for _ in 0 .. 10 {
<div key={smth::KEY} />
}};
}

View File

@ -0,0 +1,59 @@
error: unexpected end of input, expected `in`
--> tests/html_macro/for-fail.rs:6:9
|
6 | _ = ::yew::html!{for x};
| ^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected an expression
--> tests/html_macro/for-fail.rs:7:9
|
7 | _ = ::yew::html!{for x in};
| ^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected curly braces
--> tests/html_macro/for-fail.rs:8:9
|
8 | _ = ::yew::html!{for x in 0 .. 10};
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info)
error: duplicate key for a node in a `for`-loop
this will create elements with duplicate keys if the loop iterates more than once
--> tests/html_macro/for-fail.rs:18:18
|
18 | <div key="duplicate" />
| ^^^^^^^^^^^
error: duplicate key for a node in a `for`-loop
this will create elements with duplicate keys if the loop iterates more than once
--> tests/html_macro/for-fail.rs:22:19
|
22 | <div key={smth::KEY} />
| ^^^^
error[E0267]: `break` inside of a closure
--> tests/html_macro/for-fail.rs:14:16
|
13 | _ = ::yew::html!{for _ in 0 .. 10 {
| _________-
14 | | <span>{break}</span>
| | ^^^^^ cannot `break` inside of a closure
15 | | }};
| |______- enclosing closure
error[E0308]: mismatched types
--> tests/html_macro/for-fail.rs:9:26
|
9 | _ = ::yew::html!{for (x, y) in 0 .. 10 {
| ^^^^^^
| |
| expected integer, found `(_, _)`
| expected due to this
|
= note: expected type `{integer}`
found tuple `(_, _)`

View File

@ -0,0 +1,76 @@
#![no_implicit_prelude]
// Shadow primitives
#[allow(non_camel_case_types)]
pub struct bool;
#[allow(non_camel_case_types)]
pub struct char;
#[allow(non_camel_case_types)]
pub struct f32;
#[allow(non_camel_case_types)]
pub struct f64;
#[allow(non_camel_case_types)]
pub struct i128;
#[allow(non_camel_case_types)]
pub struct i16;
#[allow(non_camel_case_types)]
pub struct i32;
#[allow(non_camel_case_types)]
pub struct i64;
#[allow(non_camel_case_types)]
pub struct i8;
#[allow(non_camel_case_types)]
pub struct isize;
#[allow(non_camel_case_types)]
pub struct str;
#[allow(non_camel_case_types)]
pub struct u128;
#[allow(non_camel_case_types)]
pub struct u16;
#[allow(non_camel_case_types)]
pub struct u32;
#[allow(non_camel_case_types)]
pub struct u64;
#[allow(non_camel_case_types)]
pub struct u8;
#[allow(non_camel_case_types)]
pub struct usize;
fn main() {
_ = ::yew::html!{
for i in 0 .. 10 {
<span>{i}</span>
}
};
struct Pair {
value1: &'static ::std::primitive::str,
value2: ::std::primitive::i32
}
_ = ::yew::html! {
for Pair { value1, value2 } in ::std::iter::Iterator::map(0 .. 10, |value2| Pair { value1: "Yew", value2 }) {
<span>{value1}</span>
<span>{value2}</span>
}
};
fn rand_number() -> ::std::primitive::u32 {
4 // chosen by fair dice roll. guaranteed to be random.
}
_ = ::yew::html!{
for _ in 0..5 {
<div>
{{
loop {
let a = rand_number();
if a % 2 == 0 {
break a;
}
}
}}
</div>
}
}
}

View File

@ -3,14 +3,14 @@ use yew::prelude::*;
fn compile_fail() {
html! { for };
html! { for () };
html! { for {()} };
html! { {for ()} };
html! { for Vec::<()>::new().into_iter() };
let empty = Vec::<()>::new().into_iter();
html! { for empty };
let empty = Vec::<()>::new();
html! { for empty.iter() };
html! { {for empty.iter()} };
html! {
<>

View File

@ -1,67 +1,47 @@
error: expected an expression after the keyword `for`
--> tests/html_macro/iterable-fail.rs:4:13
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/html_macro/iterable-fail.rs:4:5
|
4 | html! { for };
| ^^^
| ^^^^^^^^^^^^^
|
= note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: `()` is not an iterator
--> tests/html_macro/iterable-fail.rs:5:17
error: unexpected end of input, expected `in`
--> tests/html_macro/iterable-fail.rs:5:5
|
5 | html! { for () };
| ^^ `()` is not an iterator
| ^^^^^^^^^^^^^^^^
|
= help: the trait `Iterator` is not implemented for `()`, which is required by `(): IntoIterator`
= note: required for `()` to implement `IntoIterator`
= note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: `()` is not an iterator
--> tests/html_macro/iterable-fail.rs:6:17
|
6 | html! { for {()} };
| ^--^
| ||
| |this tail expression is of type `()`
| `()` is not an iterator
|
= help: the trait `Iterator` is not implemented for `()`, which is required by `(): IntoIterator`
= note: required for `()` to implement `IntoIterator`
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> tests/html_macro/iterable-fail.rs:7:17
error: expected `in`
--> tests/html_macro/iterable-fail.rs:7:33
|
7 | html! { for Vec::<()>::new().into_iter() };
| ^^^ `()` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `()`, which is required by `VNode: FromIterator<_>`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= help: the trait `FromIterator<A>` is implemented for `VNode`
= note: required for `()` to implement `ToString`
= note: required for `VNode` to implement `From<()>`
= note: required for `()` to implement `Into<VNode>`
= note: required for `VNode` to implement `FromIterator<()>`
note: required by a bound in `collect`
--> $RUST/core/src/iter/traits/iterator.rs
| ^
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> tests/html_macro/iterable-fail.rs:10:17
error: unexpected end of input, expected `in`
--> tests/html_macro/iterable-fail.rs:10:5
|
10 | html! { for empty };
| ^^^^^ `()` cannot be formatted with the default formatter
| ^^^^^^^^^^^^^^^^^^^
|
= help: the trait `std::fmt::Display` is not implemented for `()`, which is required by `VNode: FromIterator<_>`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= help: the trait `FromIterator<A>` is implemented for `VNode`
= note: required for `()` to implement `ToString`
= note: required for `VNode` to implement `From<()>`
= note: required for `()` to implement `Into<VNode>`
= note: required for `VNode` to implement `FromIterator<()>`
note: required by a bound in `collect`
--> $RUST/core/src/iter/traits/iterator.rs
= note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: `()` is not an iterator
--> tests/html_macro/iterable-fail.rs:6:18
|
6 | html! { {for ()} };
| ^^ `()` is not an iterator
|
= help: the trait `Iterator` is not implemented for `()`, which is required by `(): IntoIterator`
= note: required for `()` to implement `IntoIterator`
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> tests/html_macro/iterable-fail.rs:13:17
--> tests/html_macro/iterable-fail.rs:13:18
|
13 | html! { for empty.iter() };
| ^^^^^ `()` cannot be formatted with the default formatter
13 | html! { {for empty.iter()} };
| ^^^^^ `()` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `()`, which is required by `VNode: FromIterator<_>`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

View File

@ -45,13 +45,19 @@ fn empty_iter() -> impl ::std::iter::Iterator<Item = ::yew::Html> {
}
fn main() {
_ = ::yew::html! { for empty_iter() };
_ = ::yew::html! { for { empty_iter() } };
_ = ::yew::html! { {for empty_iter()} };
_ = ::yew::html! { {for { empty_iter() }} };
let empty = empty_vec();
_ = ::yew::html! { for empty };
_ = ::yew::html! { {for empty} };
_ = ::yew::html! { for empty_vec() };
_ = ::yew::html! { for ::std::iter::IntoIterator::into_iter(empty_vec()) };
_ = ::yew::html! { for ::std::iter::Iterator::map(0..3, |num| { ::yew::html! { <span>{ num }</span> } }) };
_ = ::yew::html! {<>
{for empty_vec()}
</>};
_ = ::yew::html! {<>
{for ::std::iter::IntoIterator::into_iter(empty_vec())}
</>};
_ = ::yew::html! {<>
{for ::std::iter::Iterator::map(0..3, |num| { ::yew::html! { <span>{ num }</span> } })}
</>};
}

View File

@ -33,6 +33,8 @@ pub use self::key::Key;
pub use self::listeners::*;
#[doc(inline)]
pub use self::vcomp::{VChild, VComp};
#[doc(hidden)]
pub use self::vlist::FullyKeyedState;
#[doc(inline)]
pub use self::vlist::VList;
#[doc(inline)]

View File

@ -5,8 +5,9 @@ use std::rc::Rc;
use super::{Key, VNode};
use crate::html::ImplicitClone;
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq)]
enum FullyKeyedState {
pub enum FullyKeyedState {
KnownFullyKeyed,
KnownMissingKeys,
Unknown,
@ -81,6 +82,20 @@ impl VList {
vlist
}
#[doc(hidden)]
/// Used by `html!` to avoid calling `.recheck_fully_keyed()` when possible.
pub fn __macro_new(
children: Vec<VNode>,
key: Option<Key>,
fully_keyed: FullyKeyedState,
) -> Self {
VList {
children: Some(Rc::new(children)),
fully_keyed,
key,
}
}
// Returns a mutable reference to children, allocates the children if it hasn't been done.
//
// This method does not reassign key state. So it should only be used internally.

View File

@ -812,7 +812,6 @@ async fn test_duplicate_suspension() {
yew::Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
.render();
sleep(Duration::from_millis(50)).await;
let result = obtain_result();
assert_eq!(result.as_str(), "hello!");

View File

@ -137,7 +137,7 @@ fn List(props: &Props) -> Html {
props.value = format!("item-{}", props.value);
item
});
html! { for modified_children }
html! {{for modified_children}}
}
html! {

View File

@ -7,12 +7,47 @@ import TabItem from '@theme/TabItem'
## Iterators
Yew supports two different syntaxes for building HTML from an iterator.
There are 3 ways to build HTML from iterators:
<Tabs>
<TabItem value="Syntax type 1" label="Syntax type 1">
<TabItem value="`for` loops" label="`for` loops">
The main approach is to use for loops, the same for loops that already exist in Rust, but with 2 key differences:
1. Unlike standard for loops which can't return anything, for loops in `html!` are converted to a list of nodes;
2. Diverging expressions, i.e. `break`, `continue` are not allowed in the body of for loops in `html!`.
The first is to call `collect::<Html>()` on the final transform in your iterator, which returns a
```rust
use yew::prelude::*;
html! {
for i in 0 .. 10 {
<span>{i}</span>
}
};
```
</TabItem>
<TabItem value="`for` block" label="`for` block">
An alternative is to use the `for` keyword, which is not native Rust syntax and instead is used by
the HTML macro to output the needed code to display the iterator.
This approach is better than the first one when the iterator is already computed and the only thing left to do
is to pass it to the macro.
```rust
use yew::prelude::*;
let items = (1..=10).collect::<Vec<_>>();
html! {
<ul class="item-list">
{ for items.iter() }
</ul>
};
```
</TabItem>
<TabItem value="`collect` method" label="`collect` method">
The last is to call `collect::<Html>()` on the final transform in your iterator, which returns a
list that Yew can display.
```rust
@ -25,24 +60,6 @@ html! {
{ items.iter().collect::<Html>() }
</ul>
};
```
</TabItem>
<TabItem value="Syntax type 2" label="Syntax type 2">
The alternative is to use the `for` keyword, which is not native Rust syntax and instead is used by
the HTML macro to output the needed code to display the iterator.
```rust
use yew::prelude::*;
let items = (1..=10).collect::<Vec<_>>();
html! {
<ul class="item-list">
{ for items.iter() }
</ul>
};
```
</TabItem>