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 commit 75b1a85c28.

* 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:
Cecile Tonglet 2021-11-22 19:13:03 +01:00 committed by GitHub
parent e92eaeda01
commit 3858aededd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 510 additions and 6 deletions

View File

@ -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),
}
}
}

View File

@ -1,15 +1,18 @@
use crate::PeekValue; use crate::PeekValue;
use proc_macro2::{Ident, Span, TokenStream}; use proc_macro2::{Delimiter, Ident, Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens}; use quote::{quote, quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::ext::IdentExt; use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Result}; use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::Token; use syn::Token;
use syn::{braced, token};
mod html_block; mod html_block;
mod html_component; mod html_component;
mod html_dashed_name; mod html_dashed_name;
mod html_element; mod html_element;
mod html_if;
mod html_iterable; mod html_iterable;
mod html_list; mod html_list;
mod html_node; mod html_node;
@ -20,6 +23,7 @@ use html_block::HtmlBlock;
use html_component::HtmlComponent; use html_component::HtmlComponent;
pub use html_dashed_name::HtmlDashedName; pub use html_dashed_name::HtmlDashedName;
use html_element::HtmlElement; use html_element::HtmlElement;
use html_if::HtmlIf;
use html_iterable::HtmlIterable; use html_iterable::HtmlIterable;
use html_list::HtmlList; use html_list::HtmlList;
use html_node::HtmlNode; use html_node::HtmlNode;
@ -30,6 +34,7 @@ pub enum HtmlType {
Component, Component,
List, List,
Element, Element,
If,
Empty, Empty,
} }
@ -38,11 +43,12 @@ pub enum HtmlTree {
Component(Box<HtmlComponent>), Component(Box<HtmlComponent>),
List(Box<HtmlList>), List(Box<HtmlList>),
Element(Box<HtmlElement>), Element(Box<HtmlElement>),
If(Box<HtmlIf>),
Empty, Empty,
} }
impl Parse for HtmlTree { 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) let html_type = Self::peek_html_type(input)
.ok_or_else(|| input.error("expected a valid html element"))?; .ok_or_else(|| input.error("expected a valid html element"))?;
let html_tree = match html_type { let html_tree = match html_type {
@ -51,6 +57,7 @@ impl Parse for HtmlTree {
HtmlType::Element => HtmlTree::Element(Box::new(input.parse()?)), HtmlType::Element => HtmlTree::Element(Box::new(input.parse()?)),
HtmlType::Block => HtmlTree::Block(Box::new(input.parse()?)), HtmlType::Block => HtmlTree::Block(Box::new(input.parse()?)),
HtmlType::List => HtmlTree::List(Box::new(input.parse()?)), HtmlType::List => HtmlTree::List(Box::new(input.parse()?)),
HtmlType::If => HtmlTree::If(Box::new(input.parse()?)),
}; };
Ok(html_tree) Ok(html_tree)
} }
@ -72,6 +79,8 @@ impl HtmlTree {
.is_some() .is_some()
{ {
Some(HtmlType::Block) Some(HtmlType::Block)
} else if HtmlIf::peek(input.cursor()).is_some() {
Some(HtmlType::If)
} else if input.peek(Token![<]) { } else if input.peek(Token![<]) {
let _lt: Token![<] = input.parse().ok()?; let _lt: Token![<] = input.parse().ok()?;
@ -117,6 +126,7 @@ impl ToTokens for HtmlTree {
HtmlTree::Element(tag) => tag.to_tokens(tokens), HtmlTree::Element(tag) => tag.to_tokens(tokens),
HtmlTree::List(list) => list.to_tokens(tokens), HtmlTree::List(list) => list.to_tokens(tokens),
HtmlTree::Block(block) => block.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 { 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() { let html_root = if HtmlTree::peek_html_type(input).is_some() {
Self::Tree(input.parse()?) Self::Tree(input.parse()?)
} else if HtmlIterable::peek(input.cursor()).is_some() { } else if HtmlIterable::peek(input.cursor()).is_some() {
@ -162,7 +172,7 @@ impl ToTokens for HtmlRoot {
/// Same as HtmlRoot but always returns a VNode. /// Same as HtmlRoot but always returns a VNode.
pub struct HtmlRootVNode(HtmlRoot); pub struct HtmlRootVNode(HtmlRoot);
impl Parse for HtmlRootVNode { impl Parse for HtmlRootVNode {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse().map(Self) input.parse().map(Self)
} }
} }
@ -202,7 +212,7 @@ impl HtmlChildrenTree {
Self(Vec::new()) 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()?); self.0.push(input.parse()?);
Ok(()) 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 { impl ToTokens for HtmlChildrenTree {
@ -261,3 +281,38 @@ impl ToTokens for HtmlChildrenTree {
tokens.extend(self.to_build_vec_token_stream()); 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)
)
}
});
}
}

View File

@ -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() {}

View File

@ -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

View File

@ -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() {}

View File

@ -816,4 +816,49 @@ mod layout_tests {
layout10, layout11, layout12, 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]);
}
} }

View File

@ -1227,3 +1227,152 @@ mod layout_tests {
diff_layouts(vec![layout1, layout2, layout3, layout4]); 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> },
);
}
}

View File

@ -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. 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.
:::