mirror of https://github.com/yewstack/yew
Add the ability to add child nodes conditionally in `html!` (#1609)
* Initial commit Forked at:66d506e133
Parent branch: origin/master * Add the ability to add child nodes conditionally in html! * WIP Forked at:66d506e133
Parent branch: origin/master * CLEANUP Forked at:66d506e133
Parent branch: origin/master * Experiment * Failing test * More tests * More tests * Add new HtmlIterable with syntax `for {...}` instead of `{for ...}` * Remove HtmlIf from HtmlRoot (already done in HtmlTree) * WIP * WIP * WIP * WIP * Revert * CLEANUP * WIP * CLEANUP * Remove IterableNew * Renamed HtmlBranch to HtmlRootBraced and moved to mod.rs * Update yew-macro/tests/html_macro/html-if-pass.rs Co-authored-by: Simon <simon@siku2.io> * Suggestion * Oops * Added ToNodeIterator to HtmlIf * Improve error spans * More html!() * Move tests to not use browser * Multiple children in if-expr * Clippy fix * Clippy fix again * Re-trigger CI * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * Replacing ParseResult by syn::Result everywhere * Remove unnecessary &mut * Attempt to add test on ToNodeIterator * Clippy fixes * Still works for some reason * Revert "Attempt to add test on ToNodeIterator" This reverts commit75b1a85c28
. * fix CI * add docs on website * Apply suggestions from code review Co-authored-by: mc1098 <m.cripps1@uni.brighton.ac.uk> * apparently I can't hide lines on website * update stderr file * will this work? * fix bug where conditions can't be expressions * better error message * clippy & fmt Co-authored-by: Simon <simon@siku2.io> Co-authored-by: Hamza <muhammadhamza1311@gmail.com> Co-authored-by: mc1098 <m.cripps1@uni.brighton.ac.uk>
This commit is contained in:
parent
e92eaeda01
commit
3858aededd
|
@ -0,0 +1,140 @@
|
|||
use super::{HtmlRootBraced, ToNodeIterator};
|
||||
use crate::PeekValue;
|
||||
use boolinator::Boolinator;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote_spanned, ToTokens};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Expr, Token};
|
||||
|
||||
pub struct HtmlIf {
|
||||
if_token: Token![if],
|
||||
cond: Box<Expr>,
|
||||
then_branch: HtmlRootBraced,
|
||||
else_branch: Option<(Token![else], Box<HtmlRootBracedOrIf>)>,
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlIf {
|
||||
fn peek(cursor: Cursor) -> Option<()> {
|
||||
let (ident, _) = cursor.ident()?;
|
||||
(ident == "if").as_option()
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlIf {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let if_token = input.parse()?;
|
||||
let cond = Box::new(input.call(Expr::parse_without_eager_brace)?);
|
||||
match &*cond {
|
||||
Expr::Block(syn::ExprBlock { block, .. }) if block.stmts.is_empty() => {
|
||||
return Err(syn::Error::new(
|
||||
cond.span(),
|
||||
"missing condition for `if` expression",
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
cond.span(),
|
||||
"this `if` expression has a condition, but no block",
|
||||
));
|
||||
}
|
||||
|
||||
let then_branch = input.parse()?;
|
||||
let else_branch = input
|
||||
.parse::<Token![else]>()
|
||||
.ok()
|
||||
.map(|else_token| {
|
||||
if input.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
else_token.span(),
|
||||
"expected block or `if` after `else`",
|
||||
));
|
||||
}
|
||||
|
||||
input.parse().map(|branch| (else_token, branch))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(HtmlIf {
|
||||
if_token,
|
||||
cond,
|
||||
then_branch,
|
||||
else_branch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlIf {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let HtmlIf {
|
||||
if_token,
|
||||
cond,
|
||||
then_branch,
|
||||
else_branch,
|
||||
} = self;
|
||||
let default_else_branch = syn::parse_quote! { {} };
|
||||
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
|
||||
};
|
||||
|
||||
tokens.extend(new_tokens);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlRootBracedOrIf {
|
||||
fn peek(cursor: Cursor) -> Option<()> {
|
||||
HtmlRootBraced::peek(cursor).or_else(|| HtmlIf::peek(cursor))
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlRootBracedOrIf {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if HtmlRootBraced::peek(input.cursor()).is_some() {
|
||||
input.parse().map(Self::Branch)
|
||||
} else {
|
||||
input.parse().map(Self::If)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlRootBracedOrIf {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
Self::Branch(x) => x.to_tokens(tokens),
|
||||
Self::If(x) => x.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,18 @@
|
|||
use crate::PeekValue;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use proc_macro2::{Delimiter, Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::buffer::Cursor;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::parse::{Parse, ParseStream, Result};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::Token;
|
||||
use syn::{braced, token};
|
||||
|
||||
mod html_block;
|
||||
mod html_component;
|
||||
mod html_dashed_name;
|
||||
mod html_element;
|
||||
mod html_if;
|
||||
mod html_iterable;
|
||||
mod html_list;
|
||||
mod html_node;
|
||||
|
@ -20,6 +23,7 @@ use html_block::HtmlBlock;
|
|||
use html_component::HtmlComponent;
|
||||
pub use html_dashed_name::HtmlDashedName;
|
||||
use html_element::HtmlElement;
|
||||
use html_if::HtmlIf;
|
||||
use html_iterable::HtmlIterable;
|
||||
use html_list::HtmlList;
|
||||
use html_node::HtmlNode;
|
||||
|
@ -30,6 +34,7 @@ pub enum HtmlType {
|
|||
Component,
|
||||
List,
|
||||
Element,
|
||||
If,
|
||||
Empty,
|
||||
}
|
||||
|
||||
|
@ -38,11 +43,12 @@ pub enum HtmlTree {
|
|||
Component(Box<HtmlComponent>),
|
||||
List(Box<HtmlList>),
|
||||
Element(Box<HtmlElement>),
|
||||
If(Box<HtmlIf>),
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Parse for HtmlTree {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
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 {
|
||||
|
@ -51,6 +57,7 @@ impl Parse for HtmlTree {
|
|||
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)
|
||||
}
|
||||
|
@ -72,6 +79,8 @@ impl HtmlTree {
|
|||
.is_some()
|
||||
{
|
||||
Some(HtmlType::Block)
|
||||
} else if HtmlIf::peek(input.cursor()).is_some() {
|
||||
Some(HtmlType::If)
|
||||
} else if input.peek(Token![<]) {
|
||||
let _lt: Token![<] = input.parse().ok()?;
|
||||
|
||||
|
@ -117,6 +126,7 @@ impl ToTokens for HtmlTree {
|
|||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +138,7 @@ pub enum HtmlRoot {
|
|||
}
|
||||
|
||||
impl Parse for HtmlRoot {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
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() {
|
||||
|
@ -162,7 +172,7 @@ impl ToTokens for HtmlRoot {
|
|||
/// Same as HtmlRoot but always returns a VNode.
|
||||
pub struct HtmlRootVNode(HtmlRoot);
|
||||
impl Parse for HtmlRootVNode {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
input.parse().map(Self)
|
||||
}
|
||||
}
|
||||
|
@ -202,7 +212,7 @@ impl HtmlChildrenTree {
|
|||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub fn parse_child(&mut self, input: ParseStream) -> Result<()> {
|
||||
pub fn parse_child(&mut self, input: ParseStream) -> syn::Result<()> {
|
||||
self.0.push(input.parse()?);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -254,6 +264,16 @@ impl HtmlChildrenTree {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_delimited(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut children = HtmlChildrenTree::new();
|
||||
|
||||
while !input.is_empty() {
|
||||
children.parse_child(input)?;
|
||||
}
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlChildrenTree {
|
||||
|
@ -261,3 +281,38 @@ impl ToTokens for HtmlChildrenTree {
|
|||
tokens.extend(self.to_build_vec_token_stream());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HtmlRootBraced {
|
||||
brace: token::Brace,
|
||||
children: HtmlChildrenTree,
|
||||
}
|
||||
|
||||
impl PeekValue<()> for HtmlRootBraced {
|
||||
fn peek(cursor: Cursor) -> Option<()> {
|
||||
cursor.group(Delimiter::Brace).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HtmlRootBraced {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let content;
|
||||
let brace = braced!(content in input);
|
||||
let children = HtmlChildrenTree::parse_delimited(&content)?;
|
||||
|
||||
Ok(HtmlRootBraced { brace, children })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HtmlRootBraced {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self { brace, children } = self;
|
||||
|
||||
tokens.extend(quote_spanned! {brace.span.span()=>
|
||||
{
|
||||
::yew::virtual_dom::VNode::VList(
|
||||
::yew::virtual_dom::VList::with_children(#children, ::std::option::Option::None)
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
fn compile_fail() {
|
||||
html! { if {} };
|
||||
html! { if 42 {} };
|
||||
html! { if true {} else };
|
||||
html! { if true {} else if {} };
|
||||
html! { if true {} else if true {} else };
|
||||
html! { if true {} else if true {} else };
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,35 @@
|
|||
error: missing condition for `if` expression
|
||||
--> tests/html_macro/html-if-fail.rs:4:16
|
||||
|
|
||||
4 | html! { if {} };
|
||||
| ^^
|
||||
|
||||
error: expected block or `if` after `else`
|
||||
--> tests/html_macro/html-if-fail.rs:6:24
|
||||
|
|
||||
6 | html! { if true {} else };
|
||||
| ^^^^
|
||||
|
||||
error: missing condition for `if` expression
|
||||
--> tests/html_macro/html-if-fail.rs:7:32
|
||||
|
|
||||
7 | html! { if true {} else if {} };
|
||||
| ^^
|
||||
|
||||
error: expected block or `if` after `else`
|
||||
--> tests/html_macro/html-if-fail.rs:8:40
|
||||
|
|
||||
8 | html! { if true {} else if true {} else };
|
||||
| ^^^^
|
||||
|
||||
error: expected block or `if` after `else`
|
||||
--> tests/html_macro/html-if-fail.rs:9:40
|
||||
|
|
||||
9 | html! { if true {} else if true {} else };
|
||||
| ^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> tests/html_macro/html-if-fail.rs:5:16
|
||||
|
|
||||
5 | html! { if 42 {} };
|
||||
| ^^ expected `bool`, found integer
|
|
@ -0,0 +1,35 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
fn compile_pass_lit() {
|
||||
html! { if true {} };
|
||||
html! { if true { <div/> } };
|
||||
html! { if true { <div/><div/> } };
|
||||
html! { if true { <><div/><div/></> } };
|
||||
html! { if true { { html! {} } } };
|
||||
html! { if true { { { let _x = 42; html! {} } } } };
|
||||
html! { if true {} else {} };
|
||||
html! { if true {} else if true {} };
|
||||
html! { if true {} else if true {} else {} };
|
||||
html! { if let Some(text) = Some("text") { <span>{ text }</span> } };
|
||||
html! { <><div/>if true {}<div/></> };
|
||||
html! { <div>if true {}</div> };
|
||||
}
|
||||
|
||||
fn compile_pass_expr() {
|
||||
let condition = true;
|
||||
|
||||
html! { if condition {} };
|
||||
html! { if condition { <div/> } };
|
||||
html! { if condition { <div/><div/> } };
|
||||
html! { if condition { <><div/><div/></> } };
|
||||
html! { if condition { { html! {} } } };
|
||||
html! { if condition { { { let _x = 42; html! {} } } } };
|
||||
html! { if condition {} else {} };
|
||||
html! { if condition {} else if condition {} };
|
||||
html! { if condition {} else if condition {} else {} };
|
||||
html! { if let Some(text) = Some("text") { <span>{ text }</span> } };
|
||||
html! { <><div/>if condition {}<div/></> };
|
||||
html! { <div>if condition {}</div> };
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -816,4 +816,49 @@ mod layout_tests {
|
|||
layout10, layout11, layout12,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_with_children() {
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct Props {
|
||||
children: Children,
|
||||
}
|
||||
|
||||
struct ComponentWithChildren;
|
||||
|
||||
impl Component for ComponentWithChildren {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<ul>
|
||||
{ for ctx.props().children.iter().map(|child| html! { <li>{ child }</li> }) }
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layout = TestLayout {
|
||||
name: "13",
|
||||
node: html! {
|
||||
<ComponentWithChildren>
|
||||
if true {
|
||||
<span>{ "hello" }</span>
|
||||
<span>{ "world" }</span>
|
||||
} else {
|
||||
<span>{ "goodbye" }</span>
|
||||
<span>{ "world" }</span>
|
||||
}
|
||||
</ComponentWithChildren>
|
||||
},
|
||||
expected: "<ul><li><span>hello</span><span>world</span></li></ul>",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1227,3 +1227,152 @@ mod layout_tests {
|
|||
diff_layouts(vec![layout1, layout2, layout3, layout4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_without_browser {
|
||||
use crate::html;
|
||||
|
||||
#[test]
|
||||
fn html_if_bool() {
|
||||
assert_eq!(
|
||||
html! {
|
||||
if true {
|
||||
<div class="foo" />
|
||||
}
|
||||
},
|
||||
html! { <div class="foo" /> },
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
if false {
|
||||
<div class="foo" />
|
||||
} else {
|
||||
<div class="bar" />
|
||||
}
|
||||
},
|
||||
html! {
|
||||
<div class="bar" />
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
if false {
|
||||
<div class="foo" />
|
||||
}
|
||||
},
|
||||
html! {},
|
||||
);
|
||||
|
||||
// non-root tests
|
||||
assert_eq!(
|
||||
html! {
|
||||
<div>
|
||||
if true {
|
||||
<div class="foo" />
|
||||
}
|
||||
</div>
|
||||
},
|
||||
html! {
|
||||
<div>
|
||||
<div class="foo" />
|
||||
</div>
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
<div>
|
||||
if false {
|
||||
<div class="foo" />
|
||||
} else {
|
||||
<div class="bar" />
|
||||
}
|
||||
</div>
|
||||
},
|
||||
html! {
|
||||
<div>
|
||||
<div class="bar" />
|
||||
</div>
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
<div>
|
||||
if false {
|
||||
<div class="foo" />
|
||||
}
|
||||
</div>
|
||||
},
|
||||
html! {
|
||||
<div>
|
||||
<></>
|
||||
</div>
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_if_option() {
|
||||
let option_foo = Some("foo");
|
||||
let none: Option<&'static str> = None;
|
||||
assert_eq!(
|
||||
html! {
|
||||
if let Some(class) = option_foo {
|
||||
<div class={class} />
|
||||
}
|
||||
},
|
||||
html! { <div class="foo" /> },
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
if let Some(class) = none {
|
||||
<div class={class} />
|
||||
} else {
|
||||
<div class="bar" />
|
||||
}
|
||||
},
|
||||
html! { <div class="bar" /> },
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
if let Some(class) = none {
|
||||
<div class={class} />
|
||||
}
|
||||
},
|
||||
html! {},
|
||||
);
|
||||
|
||||
// non-root tests
|
||||
assert_eq!(
|
||||
html! {
|
||||
<div>
|
||||
if let Some(class) = option_foo {
|
||||
<div class={class} />
|
||||
}
|
||||
</div>
|
||||
},
|
||||
html! { <div><div class="foo" /></div> },
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
<div>
|
||||
if let Some(class) = none {
|
||||
<div class={class} />
|
||||
} else {
|
||||
<div class="bar" />
|
||||
}
|
||||
</div>
|
||||
},
|
||||
html! { <div><div class="bar" /></div> },
|
||||
);
|
||||
assert_eq!(
|
||||
html! {
|
||||
<div>
|
||||
if let Some(class) = none {
|
||||
<div class={class} />
|
||||
}
|
||||
</div>
|
||||
},
|
||||
html! { <div><></></div> },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,3 +150,36 @@ The documentation for keys is yet to be written. See [#1263](https://github.com/
|
|||
|
||||
For now, use keys when you have a list where the order of elements changes. This includes inserting or removing elements from anywhere but the end of the list.
|
||||
:::
|
||||
|
||||
## If blocks
|
||||
|
||||
To conditionally render some markup, we wrap it in an `if` block:
|
||||
|
||||
```rust
|
||||
use yew::html;
|
||||
|
||||
html! {
|
||||
if true {
|
||||
<p>{ "True case" }</p>
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
There may also be an `else` case:
|
||||
|
||||
```rust
|
||||
use yew::html;
|
||||
let some_condition = true;
|
||||
|
||||
html! {
|
||||
if false {
|
||||
<p>{ "True case" }</p>
|
||||
} else {
|
||||
<p>{ "False case" }</p>
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
:::note
|
||||
`if let` statements can also be used in the same way.
|
||||
:::
|
||||
|
|
Loading…
Reference in New Issue