mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Add for-loops to html! (#3498)
This commit is contained in:
parent
8945ab7856
commit
fd96a9abca
@ -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()} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -118,7 +118,7 @@ pub fn Post(props: &Props) -> Html {
|
||||
render_quote(quote)
|
||||
}
|
||||
});
|
||||
html! { for parts }
|
||||
html! {{for parts}}
|
||||
};
|
||||
|
||||
let keywords = post
|
||||
|
||||
@ -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)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -137,6 +137,6 @@ impl Post {
|
||||
self.render_quote(quote)
|
||||
}
|
||||
});
|
||||
html! { for parts }
|
||||
html! {{for parts}}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ use crate::props::ComponentProps;
|
||||
|
||||
pub struct HtmlComponent {
|
||||
ty: Type,
|
||||
props: ComponentProps,
|
||||
pub props: ComponentProps,
|
||||
children: HtmlChildrenTree,
|
||||
close: Option<HtmlComponentClose>,
|
||||
}
|
||||
|
||||
115
packages/yew-macro/src/html_tree/html_for.rs
Normal file
115
packages/yew-macro/src/html_tree/html_for.rs
Normal 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
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -58,4 +58,8 @@ impl ToNodeIterator for HtmlIterable {
|
||||
::yew::utils::into_node_iter(#expr)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_singular(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -35,7 +35,7 @@ impl ToTokens for BaseExpr {
|
||||
}
|
||||
|
||||
pub struct ComponentProps {
|
||||
props: Props,
|
||||
pub props: Props,
|
||||
base_expr: Option<Expr>,
|
||||
}
|
||||
impl ComponentProps {
|
||||
|
||||
@ -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
|
||||
|
||||
24
packages/yew-macro/tests/html_macro/for-fail.rs
Normal file
24
packages/yew-macro/tests/html_macro/for-fail.rs
Normal 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} />
|
||||
}};
|
||||
}
|
||||
59
packages/yew-macro/tests/html_macro/for-fail.stderr
Normal file
59
packages/yew-macro/tests/html_macro/for-fail.stderr
Normal 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 `(_, _)`
|
||||
76
packages/yew-macro/tests/html_macro/for-pass.rs
Normal file
76
packages/yew-macro/tests/html_macro/for-pass.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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! {
|
||||
<>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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> } })}
|
||||
</>};
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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!");
|
||||
|
||||
@ -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! {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user