Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add for-loops to html! #3498

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
6 changes: 5 additions & 1 deletion examples/function_router/src/components/pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()} />
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/function_router/src/pages/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ pub fn Post(props: &Props) -> Html {
render_quote(quote)
}
});
html! { for parts }
html! {{for parts}}
};

let keywords = post
Expand Down
6 changes: 5 additions & 1 deletion examples/router/src/components/pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/router/src/pages/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,6 @@ impl Post {
self.render_quote(quote)
}
});
html! { for parts }
html! {{for parts}}
}
}
7 changes: 7 additions & 0 deletions packages/yew-macro/src/html_tree/html_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
2 changes: 1 addition & 1 deletion packages/yew-macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::props::ComponentProps;

pub struct HtmlComponent {
ty: Type,
props: ComponentProps,
pub props: ComponentProps,
children: HtmlChildrenTree,
close: Option<HtmlComponentClose>,
}
Expand Down
111 changes: 111 additions & 0 deletions packages/yew-macro/src/html_tree/html_for.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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: reduce nesting by using if-let guards / let-else statements once MSRV is raised
for child in body.0.iter() {
if let HtmlTree::Element(element) = child {
if let Some(key) = &element.props.special.key {
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
}))
}
}
29 changes: 4 additions & 25 deletions packages/yew-macro/src/html_tree/html_if.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions packages/yew-macro/src/html_tree/html_iterable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ impl ToNodeIterator for HtmlIterable {
::yew::utils::into_node_iter(#expr)
})
}

fn is_singular(&self) -> bool {
false
}
}
29 changes: 18 additions & 11 deletions packages/yew-macro/src/html_tree/html_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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 }
};

tokens.extend(quote_spanned! {spanned.span()=>
::yew::virtual_dom::VNode::VList(::std::rc::Rc::new(
}
.span();

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 {
Expand Down Expand Up @@ -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> {
Expand Down
11 changes: 9 additions & 2 deletions packages/yew-macro/src/html_tree/html_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,20 @@ 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)
})
}
}
}

fn is_singular(&self) -> bool {
match self {
Self::Literal(_) => true,
Self::Expression(_) => false,
}
}
}
Loading
Loading