Skip to content

Commit fd96a9a

Browse files
Add for-loops to html! (#3498)
1 parent 8945ab7 commit fd96a9a

File tree

25 files changed

+493
-145
lines changed

25 files changed

+493
-145
lines changed

examples/function_router/src/components/pagination.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ pub fn RenderLinks(props: &RenderLinksProps) -> Html {
8484
</>
8585
}
8686
} else {
87-
html! { for range.map(|page| html! {<RenderLink to_page={page} props={props.clone()} />}) }
87+
html! {
88+
for page in range {
89+
<RenderLink to_page={page} props={props.clone()} />
90+
}
91+
}
8892
}
8993
}
9094

examples/function_router/src/pages/post.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ pub fn Post(props: &Props) -> Html {
118118
render_quote(quote)
119119
}
120120
});
121-
html! { for parts }
121+
html! {{for parts}}
122122
};
123123

124124
let keywords = post

examples/router/src/components/pagination.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ impl Pagination {
8080
</>
8181
}
8282
} else {
83-
html! { for pages.map(|page| self.render_link(page, props)) }
83+
html! {
84+
for page in pages {
85+
{self.render_link(page, props)}
86+
}
87+
}
8488
}
8589
}
8690

examples/router/src/pages/post.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,6 @@ impl Post {
137137
self.render_quote(quote)
138138
}
139139
});
140-
html! { for parts }
140+
html! {{for parts}}
141141
}
142142
}

packages/yew-macro/src/html_tree/html_block.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,11 @@ impl ToNodeIterator for HtmlBlock {
5959

6060
Some(quote_spanned! {brace.span=> #new_tokens})
6161
}
62+
63+
fn is_singular(&self) -> bool {
64+
match &self.content {
65+
BlockContent::Node(node) => node.is_singular(),
66+
BlockContent::Iterable(_) => false,
67+
}
68+
}
6269
}

packages/yew-macro/src/html_tree/html_component.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::props::ComponentProps;
1111

1212
pub struct HtmlComponent {
1313
ty: Type,
14-
props: ComponentProps,
14+
pub props: ComponentProps,
1515
children: HtmlChildrenTree,
1616
close: Option<HtmlComponentClose>,
1717
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use proc_macro2::{Ident, TokenStream};
2+
use quote::{quote, ToTokens};
3+
use syn::buffer::Cursor;
4+
use syn::parse::{Parse, ParseStream};
5+
use syn::spanned::Spanned;
6+
use syn::token::{For, In};
7+
use syn::{braced, Expr, Pat};
8+
9+
use super::{HtmlChildrenTree, ToNodeIterator};
10+
use crate::html_tree::HtmlTree;
11+
use crate::PeekValue;
12+
13+
/// Determines if an expression is guaranteed to always return the same value anywhere.
14+
fn is_contextless_pure(expr: &Expr) -> bool {
15+
match expr {
16+
Expr::Lit(_) => true,
17+
Expr::Path(path) => path.path.get_ident().is_none(),
18+
_ => false,
19+
}
20+
}
21+
22+
pub struct HtmlFor {
23+
pat: Pat,
24+
iter: Expr,
25+
body: HtmlChildrenTree,
26+
}
27+
28+
impl PeekValue<()> for HtmlFor {
29+
fn peek(cursor: Cursor) -> Option<()> {
30+
let (ident, _) = cursor.ident()?;
31+
(ident == "for").then_some(())
32+
}
33+
}
34+
35+
impl Parse for HtmlFor {
36+
fn parse(input: ParseStream) -> syn::Result<Self> {
37+
For::parse(input)?;
38+
let pat = Pat::parse_single(input)?;
39+
In::parse(input)?;
40+
let iter = Expr::parse_without_eager_brace(input)?;
41+
42+
let body_stream;
43+
braced!(body_stream in input);
44+
45+
let body = HtmlChildrenTree::parse_delimited(&body_stream)?;
46+
// TODO: more concise code by using if-let guards once MSRV is raised
47+
for child in body.0.iter() {
48+
let HtmlTree::Element(element) = child else {
49+
continue;
50+
};
51+
52+
let Some(key) = &element.props.special.key else {
53+
continue;
54+
};
55+
56+
if is_contextless_pure(&key.value) {
57+
return Err(syn::Error::new(
58+
key.value.span(),
59+
"duplicate key for a node in a `for`-loop\nthis will create elements with \
60+
duplicate keys if the loop iterates more than once",
61+
));
62+
}
63+
}
64+
Ok(Self { pat, iter, body })
65+
}
66+
}
67+
68+
impl ToTokens for HtmlFor {
69+
fn to_tokens(&self, tokens: &mut TokenStream) {
70+
let Self { pat, iter, body } = self;
71+
let acc = Ident::new("__yew_v", iter.span());
72+
73+
let alloc_opt = body
74+
.size_hint()
75+
.filter(|&size| size > 1) // explicitly reserving space for 1 more element is redundant
76+
.map(|size| quote!( #acc.reserve(#size) ));
77+
78+
let vlist_gen = match body.fully_keyed() {
79+
Some(true) => quote! {
80+
::yew::virtual_dom::VList::__macro_new(
81+
#acc,
82+
::std::option::Option::None,
83+
::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed
84+
)
85+
},
86+
Some(false) => quote! {
87+
::yew::virtual_dom::VList::__macro_new(
88+
#acc,
89+
::std::option::Option::None,
90+
::yew::virtual_dom::FullyKeyedState::KnownMissingKeys
91+
)
92+
},
93+
None => quote! {
94+
::yew::virtual_dom::VList::with_children(#acc, ::std::option::Option::None)
95+
},
96+
};
97+
98+
let body = body.0.iter().map(|child| {
99+
if let Some(child) = child.to_node_iterator_stream() {
100+
quote!( #acc.extend(#child) )
101+
} else {
102+
quote!( #acc.push(::std::convert::Into::into(#child)) )
103+
}
104+
});
105+
106+
tokens.extend(quote!({
107+
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
108+
::std::iter::Iterator::for_each(
109+
::std::iter::IntoIterator::into_iter(#iter),
110+
|#pat| { #alloc_opt; #(#body);* }
111+
);
112+
#vlist_gen
113+
}))
114+
}
115+
}

packages/yew-macro/src/html_tree/html_if.rs

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ use quote::{quote_spanned, ToTokens};
33
use syn::buffer::Cursor;
44
use syn::parse::{Parse, ParseStream};
55
use syn::spanned::Spanned;
6-
use syn::{Expr, Token};
6+
use syn::{parse_quote, Expr, Token};
77

8-
use super::{HtmlRootBraced, ToNodeIterator};
8+
use super::HtmlRootBraced;
99
use crate::PeekValue;
1010

1111
pub struct HtmlIf {
@@ -69,13 +69,13 @@ impl Parse for HtmlIf {
6969

7070
impl ToTokens for HtmlIf {
7171
fn to_tokens(&self, tokens: &mut TokenStream) {
72-
let HtmlIf {
72+
let Self {
7373
if_token,
7474
cond,
7575
then_branch,
7676
else_branch,
7777
} = self;
78-
let default_else_branch = syn::parse_quote! { {} };
78+
let default_else_branch = parse_quote! { {} };
7979
let else_branch = else_branch
8080
.as_ref()
8181
.map(|(_, branch)| branch)
@@ -88,27 +88,6 @@ impl ToTokens for HtmlIf {
8888
}
8989
}
9090

91-
impl ToNodeIterator for HtmlIf {
92-
fn to_node_iterator_stream(&self) -> Option<TokenStream> {
93-
let HtmlIf {
94-
if_token,
95-
cond,
96-
then_branch,
97-
else_branch,
98-
} = self;
99-
let default_else_branch = syn::parse_str("{}").unwrap();
100-
let else_branch = else_branch
101-
.as_ref()
102-
.map(|(_, branch)| branch)
103-
.unwrap_or(&default_else_branch);
104-
let new_tokens = quote_spanned! {if_token.span()=>
105-
if #cond #then_branch else #else_branch
106-
};
107-
108-
Some(quote_spanned! {if_token.span=> #new_tokens})
109-
}
110-
}
111-
11291
pub enum HtmlRootBracedOrIf {
11392
Branch(HtmlRootBraced),
11493
If(HtmlIf),

packages/yew-macro/src/html_tree/html_iterable.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,8 @@ impl ToNodeIterator for HtmlIterable {
5858
::yew::utils::into_node_iter(#expr)
5959
})
6060
}
61+
62+
fn is_singular(&self) -> bool {
63+
false
64+
}
6165
}

packages/yew-macro/src/html_tree/html_list.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::props::Prop;
1010
use crate::{Peek, PeekValue};
1111

1212
pub struct HtmlList {
13-
open: HtmlListOpen,
13+
pub open: HtmlListOpen,
1414
pub children: HtmlChildrenTree,
1515
close: HtmlListClose,
1616
}
@@ -71,23 +71,30 @@ impl ToTokens for HtmlList {
7171
quote! { ::std::option::Option::None }
7272
};
7373

74-
let spanned = {
74+
let span = {
7575
let open = open.to_spanned();
7676
let close = close.to_spanned();
7777
quote! { #open #close }
78-
};
79-
80-
tokens.extend(quote_spanned! {spanned.span()=>
81-
::yew::virtual_dom::VNode::VList(::std::rc::Rc::new(
78+
}
79+
.span();
80+
81+
tokens.extend(match children.fully_keyed() {
82+
Some(true) => quote_spanned!{span=>
83+
::yew::virtual_dom::VList::__macro_new(#children, #key, ::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed)
84+
},
85+
Some(false) => quote_spanned!{span=>
86+
::yew::virtual_dom::VList::__macro_new(#children, #key, ::yew::virtual_dom::FullyKeyedState::KnownMissingKeys)
87+
},
88+
None => quote_spanned!{span=>
8289
::yew::virtual_dom::VList::with_children(#children, #key)
83-
))
90+
}
8491
});
8592
}
8693
}
8794

88-
struct HtmlListOpen {
95+
pub struct HtmlListOpen {
8996
tag: TagTokens,
90-
props: HtmlListProps,
97+
pub props: HtmlListProps,
9198
}
9299
impl HtmlListOpen {
93100
fn to_spanned(&self) -> impl ToTokens {
@@ -121,8 +128,8 @@ impl Parse for HtmlListOpen {
121128
}
122129
}
123130

124-
struct HtmlListProps {
125-
key: Option<Expr>,
131+
pub struct HtmlListProps {
132+
pub key: Option<Expr>,
126133
}
127134
impl Parse for HtmlListProps {
128135
fn parse(input: ParseStream) -> syn::Result<Self> {

0 commit comments

Comments
 (0)