diff --git a/packages/yew-macro/src/html_tree/html_if.rs b/packages/yew-macro/src/html_tree/html_if.rs new file mode 100644 index 000000000..aae1890ab --- /dev/null +++ b/packages/yew-macro/src/html_tree/html_if.rs @@ -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, + then_branch: HtmlRootBraced, + else_branch: Option<(Token![else], Box)>, +} + +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 { + 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::() + .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 { + 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 { + 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), + } + } +} diff --git a/packages/yew-macro/src/html_tree/mod.rs b/packages/yew-macro/src/html_tree/mod.rs index 0dd85d4c5..ad18325da 100644 --- a/packages/yew-macro/src/html_tree/mod.rs +++ b/packages/yew-macro/src/html_tree/mod.rs @@ -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), List(Box), Element(Box), + If(Box), Empty, } impl Parse for HtmlTree { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream) -> syn::Result { 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 { + fn parse(input: ParseStream) -> syn::Result { 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 { + fn parse(input: ParseStream) -> syn::Result { 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 { + 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 { + 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) + ) + } + }); + } +} diff --git a/packages/yew-macro/tests/html_macro/html-if-fail.rs b/packages/yew-macro/tests/html_macro/html-if-fail.rs new file mode 100644 index 000000000..d1722636f --- /dev/null +++ b/packages/yew-macro/tests/html_macro/html-if-fail.rs @@ -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() {} diff --git a/packages/yew-macro/tests/html_macro/html-if-fail.stderr b/packages/yew-macro/tests/html_macro/html-if-fail.stderr new file mode 100644 index 000000000..4d2089064 --- /dev/null +++ b/packages/yew-macro/tests/html_macro/html-if-fail.stderr @@ -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 diff --git a/packages/yew-macro/tests/html_macro/html-if-pass.rs b/packages/yew-macro/tests/html_macro/html-if-pass.rs new file mode 100644 index 000000000..4ad4a2657 --- /dev/null +++ b/packages/yew-macro/tests/html_macro/html-if-pass.rs @@ -0,0 +1,35 @@ +use yew::prelude::*; + +fn compile_pass_lit() { + html! { if true {} }; + html! { if true {
} }; + html! { if true {
} }; + html! { if true { <>
} }; + 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") { { text } } }; + html! { <>
if true {}
}; + html! {
if true {}
}; +} + +fn compile_pass_expr() { + let condition = true; + + html! { if condition {} }; + html! { if condition {
} }; + html! { if condition {
} }; + html! { if condition { <>
} }; + 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") { { text } } }; + html! { <>
if condition {}
}; + html! {
if condition {}
}; +} + +fn main() {} diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index df21da511..c7b58b749 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -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 + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
    + { for ctx.props().children.iter().map(|child| html! {
  • { child }
  • }) } +
+ } + } + } + + let layout = TestLayout { + name: "13", + node: html! { + + if true { + { "hello" } + { "world" } + } else { + { "goodbye" } + { "world" } + } + + }, + expected: "
  • helloworld
", + }; + + diff_layouts(vec![layout]); + } } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index ab1ce2a8d..9e6130ec0 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -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 { +
+ } + }, + html! {
}, + ); + assert_eq!( + html! { + if false { +
+ } else { +
+ } + }, + html! { +
+ }, + ); + assert_eq!( + html! { + if false { +
+ } + }, + html! {}, + ); + + // non-root tests + assert_eq!( + html! { +
+ if true { +
+ } +
+ }, + html! { +
+
+
+ }, + ); + assert_eq!( + html! { +
+ if false { +
+ } else { +
+ } +
+ }, + html! { +
+
+
+ }, + ); + assert_eq!( + html! { +
+ if false { +
+ } +
+ }, + html! { +
+ <> +
+ }, + ); + } + + #[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 { +
+ } + }, + html! {
}, + ); + assert_eq!( + html! { + if let Some(class) = none { +
+ } else { +
+ } + }, + html! {
}, + ); + assert_eq!( + html! { + if let Some(class) = none { +
+ } + }, + html! {}, + ); + + // non-root tests + assert_eq!( + html! { +
+ if let Some(class) = option_foo { +
+ } +
+ }, + html! {
}, + ); + assert_eq!( + html! { +
+ if let Some(class) = none { +
+ } else { +
+ } +
+ }, + html! {
}, + ); + assert_eq!( + html! { +
+ if let Some(class) = none { +
+ } +
+ }, + html! {
<>
}, + ); + } +} diff --git a/website/docs/concepts/html.md b/website/docs/concepts/html.md index 71596d6e7..0fd2d6e6d 100644 --- a/website/docs/concepts/html.md +++ b/website/docs/concepts/html.md @@ -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 { +

{ "True case" }

+ } +}; +``` + +There may also be an `else` case: + +```rust +use yew::html; +let some_condition = true; + +html! { + if false { +

{ "True case" }

+ } else { +

{ "False case" }

+ } +}; +``` + +:::note +`if let` statements can also be used in the same way. +:::