Improve nested html! expansion by unwrapping VNodes (#820)

* Improve nested html! expansion by omitting VNodes

* Add unittest and fix child node expansion

* Fix existing unittests and check backward compat

* Fix rebase conflicts

* Parse expression

* Fix clippy

Co-authored-by: Justin Starry <justin.starry@icloud.com>
This commit is contained in:
Konstantin Itskov 2019-12-28 23:27:23 -05:00 committed by Justin Starry
parent b801d4fbf9
commit a900fbee49
11 changed files with 232 additions and 19 deletions

View File

@ -23,7 +23,7 @@ lazy_static = "1.3.0"
proc-macro-hack = "0.5" proc-macro-hack = "0.5"
proc-macro2 = "1.0" proc-macro2 = "1.0"
quote = "1.0" quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] } syn = { version = "1.0", features = ["full", "extra-traits", "visit-mut"] }
[dev-dependencies] [dev-dependencies]
yew = { path = "../.." } yew = { path = "../.." }

View File

@ -26,7 +26,7 @@ impl PeekValue<()> for HtmlBlock {
impl Parse for HtmlBlock { impl Parse for HtmlBlock {
fn parse(input: ParseStream) -> ParseResult<Self> { fn parse(input: ParseStream) -> ParseResult<Self> {
let content; let content: syn::parse::ParseBuffer<'_>;
let brace = braced!(content in input); let brace = braced!(content in input);
let content = if HtmlIterable::peek(content.cursor()).is_some() { let content = if HtmlIterable::peek(content.cursor()).is_some() {
BlockContent::Iterable(content.parse()?) BlockContent::Iterable(content.parse()?)

View File

@ -11,7 +11,7 @@ use syn::parse;
use syn::parse::{Parse, ParseStream, Result as ParseResult}; use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::{Expr, Ident, Path, PathArguments, PathSegment, Token, Type, TypePath}; use syn::{Expr, Ident, Index, Path, PathArguments, PathSegment, Token, Type, TypePath};
pub struct HtmlComponent { pub struct HtmlComponent {
ty: Type, ty: Type,
@ -123,10 +123,17 @@ impl ToTokens for HtmlComponent {
}; };
let set_children = if !children.is_empty() { let set_children = if !children.is_empty() {
let i = (0..children.len())
.map(|x| Index::from(x))
.collect::<Vec<_>>();
quote! { quote! {
.children(::yew::html::ChildrenRenderer::new( .children(::yew::html::ChildrenRenderer::new({
vec![#(#children.into(),)*] let mut v = Vec::new();
)) let comps = (#(#children,)*);
#(::yew::utils::NodeSeq::from(comps.#i).into_iter()
.for_each(|x| v.push(x.into()));)*
v
}))
} }
} else { } else {
quote! {} quote! {}

View File

@ -7,6 +7,10 @@ use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::{Expr, Token}; use syn::{Expr, Token};
use proc_macro2::{Ident, Span};
use syn::visit_mut::{self, VisitMut};
use syn::Macro;
pub struct HtmlIterable(Expr); pub struct HtmlIterable(Expr);
impl PeekValue<()> for HtmlIterable { impl PeekValue<()> for HtmlIterable {
@ -16,12 +20,28 @@ impl PeekValue<()> for HtmlIterable {
} }
} }
struct HtmlInnerModifier;
impl VisitMut for HtmlInnerModifier {
fn visit_macro_mut(&mut self, node: &mut Macro) {
if node.path.is_ident("html") {
let ident = &mut node.path.segments.last_mut().unwrap().ident;
*ident = Ident::new("html_nested", Span::call_site());
}
// Delegate to the default impl to visit any nested functions.
visit_mut::visit_macro_mut(self, node);
}
}
impl Parse for HtmlIterable { impl Parse for HtmlIterable {
fn parse(input: ParseStream) -> ParseResult<Self> { fn parse(input: ParseStream) -> ParseResult<Self> {
let for_token = input.parse::<Token![for]>()?; let for_token = input.parse::<Token![for]>()?;
match input.parse() { match input.parse() {
Ok(expr) => Ok(HtmlIterable(expr)), Ok(mut expr) => {
HtmlInnerModifier.visit_expr_mut(&mut expr);
Ok(HtmlIterable(expr))
}
Err(err) => { Err(err) => {
if err.to_string().starts_with("unexpected end of input") { if err.to_string().starts_with("unexpected end of input") {
Err(syn::Error::new_spanned( Err(syn::Error::new_spanned(

View File

@ -4,8 +4,26 @@ use quote::{quote, quote_spanned, ToTokens};
use syn::buffer::Cursor; use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream, Result}; use syn::parse::{Parse, ParseStream, Result};
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::Expr;
use syn::Lit; use syn::Lit;
use proc_macro2::{Ident, Span};
use syn::visit_mut::{self, VisitMut};
use syn::Macro;
struct HtmlInnerModifier;
impl VisitMut for HtmlInnerModifier {
fn visit_macro_mut(&mut self, node: &mut Macro) {
if node.path.is_ident("html") {
let ident = &mut node.path.segments.last_mut().unwrap().ident;
*ident = Ident::new("html_nested", Span::call_site());
}
// Delegate to the default impl to visit any nested functions.
visit_mut::visit_macro_mut(self, node);
}
}
pub struct HtmlNode(Node); pub struct HtmlNode(Node);
impl Parse for HtmlNode { impl Parse for HtmlNode {
@ -18,7 +36,9 @@ impl Parse for HtmlNode {
} }
Node::Literal(lit) Node::Literal(lit)
} else { } else {
Node::Raw(input.parse()?) let mut expr: Expr = input.parse()?;
HtmlInnerModifier.visit_expr_mut(&mut expr);
Node::Expression(expr)
}; };
Ok(HtmlNode(node)) Ok(HtmlNode(node))
@ -46,12 +66,8 @@ impl ToTokens for HtmlNode {
impl ToTokens for Node { impl ToTokens for Node {
fn to_tokens(&self, tokens: &mut TokenStream) { fn to_tokens(&self, tokens: &mut TokenStream) {
let node_token = match &self { let node_token = match &self {
Node::Literal(lit) => quote! { Node::Literal(lit) => quote! {#lit},
::yew::virtual_dom::VNode::from(#lit) Node::Expression(expr) => quote_spanned! {expr.span()=> {#expr} },
},
Node::Raw(stream) => quote_spanned! {stream.span()=>
::yew::virtual_dom::VNode::from({#stream})
},
}; };
tokens.extend(node_token); tokens.extend(node_token);
@ -60,5 +76,5 @@ impl ToTokens for Node {
enum Node { enum Node {
Literal(Lit), Literal(Lit),
Raw(TokenStream), Expression(Expr),
} }

View File

@ -126,6 +126,19 @@ impl HtmlTree {
} }
} }
pub struct HtmlRootNested(HtmlTreeNested);
impl Parse for HtmlRootNested {
fn parse(input: ParseStream) -> Result<Self> {
Ok(HtmlRootNested(HtmlTreeNested::parse(input)?))
}
}
impl ToTokens for HtmlRootNested {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
self.0.to_tokens(tokens);
}
}
pub struct HtmlTreeNested(HtmlTree); pub struct HtmlTreeNested(HtmlTree);
impl Parse for HtmlTreeNested { impl Parse for HtmlTreeNested {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {

View File

@ -63,7 +63,7 @@ mod derive_props;
mod html_tree; mod html_tree;
use derive_props::DerivePropsInput; use derive_props::DerivePropsInput;
use html_tree::HtmlRoot; use html_tree::{HtmlRoot, HtmlRootNested};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro_hack::proc_macro_hack; use proc_macro_hack::proc_macro_hack;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
@ -94,6 +94,12 @@ pub fn derive_props(input: TokenStream) -> TokenStream {
TokenStream::from(input.into_token_stream()) TokenStream::from(input.into_token_stream())
} }
#[proc_macro_hack]
pub fn html_nested(input: TokenStream) -> TokenStream {
let root = parse_macro_input!(input as HtmlRootNested);
TokenStream::from(quote! {#root})
}
#[proc_macro_hack] #[proc_macro_hack]
pub fn html(input: TokenStream) -> TokenStream { pub fn html(input: TokenStream) -> TokenStream {
let root = parse_macro_input!(input as HtmlRoot); let root = parse_macro_input!(input as HtmlRoot);

View File

@ -72,9 +72,14 @@ use proc_macro_hack::proc_macro_hack;
#[proc_macro_hack(support_nested)] #[proc_macro_hack(support_nested)]
pub use yew_macro::html; pub use yew_macro::html;
#[doc(hidden)]
#[proc_macro_hack(support_nested)]
pub use yew_macro::html_nested;
/// This module contains macros which implements html! macro and JSX-like templates /// This module contains macros which implements html! macro and JSX-like templates
pub mod macros { pub mod macros {
pub use crate::html; pub use crate::html;
pub use crate::html_nested;
pub use yew_macro::Properties; pub use yew_macro::Properties;
} }
@ -158,6 +163,7 @@ pub mod prelude {
Renderable, ShouldRender, Renderable, ShouldRender,
}; };
pub use crate::macros::*; pub use crate::macros::*;
pub use crate::utils::NodeSeq;
pub use crate::virtual_dom::Classes; pub use crate::virtual_dom::Classes;
/// Prelude module for creating worker. /// Prelude module for creating worker.

View File

@ -3,6 +3,8 @@
use failure::{err_msg, Error}; use failure::{err_msg, Error};
use stdweb::web::document; use stdweb::web::document;
use crate::virtual_dom::VNode;
/// Returns `host` for the current document. Useful to connect to a server that server the app. /// Returns `host` for the current document. Useful to connect to a server that server the app.
pub fn host() -> Result<String, Error> { pub fn host() -> Result<String, Error> {
document() document()
@ -10,3 +12,30 @@ pub fn host() -> Result<String, Error> {
.ok_or_else(|| err_msg("can't get location")) .ok_or_else(|| err_msg("can't get location"))
.and_then(|l| l.host().map_err(Error::from)) .and_then(|l| l.host().map_err(Error::from))
} }
/// Specialty type necessary for helping flattening components returned from nested html macros.
#[derive(Debug)]
pub struct NodeSeq<T>(Vec<T>)
where
T: Into<VNode>;
impl<T: Into<VNode>> From<T> for NodeSeq<T> {
fn from(val: T) -> Self {
NodeSeq(vec![val])
}
}
impl<T: Into<VNode>> From<Vec<T>> for NodeSeq<T> {
fn from(val: Vec<T>) -> Self {
NodeSeq(val)
}
}
impl<T: Into<VNode>> IntoIterator for NodeSeq<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

View File

@ -188,13 +188,13 @@ error[E0599]: no method named `children` found for type `ChildPropertiesBuilder<
| |
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<&str>` is not satisfied
--> $DIR/html-component-fail.rs:78:5 --> $DIR/html-component-fail.rs:78:5
| |
78 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> }; 78 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<yew::virtual_dom::vnode::VNode>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<&str>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
| |
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vnode::VNode` = note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `&str`
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied

View File

@ -2,6 +2,79 @@
use yew::prelude::*; use yew::prelude::*;
use yew::html::ChildrenRenderer; use yew::html::ChildrenRenderer;
use yew::virtual_dom::{VChild, VComp, VNode};
#[derive(Clone, Debug, Properties)]
pub struct ParentProperties {
#[props(required)]
pub children: ChildrenRenderer<ParentVariant>,
}
pub struct Parent {
props: ParentProperties,
link: ComponentLink<Self>,
}
impl Component for Parent {
type Message = ();
type Properties = ParentProperties;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
return Parent { props, link };
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
unimplemented!()
}
fn view(&self) -> Html {
unimplemented!()
}
}
#[derive(Clone)]
pub enum ParentVariants {
Child(<Child as Component>::Properties),
ChildA(<ChildA as Component>::Properties),
}
impl From<ChildProperties> for ParentVariants {
fn from(props: ChildProperties) -> Self {
ParentVariants::Child(props)
}
}
impl From<ChildAProperties> for ParentVariants {
fn from(props: ChildAProperties) -> Self {
ParentVariants::ChildA(props)
}
}
#[derive(Clone)]
pub struct ParentVariant {
props: ParentVariants,
}
impl<CHILD> From<VChild<CHILD>> for ParentVariant
where
CHILD: Component,
CHILD::Properties: Into<ParentVariants>,
{
fn from(comp: VChild<CHILD>) -> Self {
return ParentVariant {
props: comp.props.into(),
};
}
}
impl Into<VNode> for ParentVariant {
fn into(self) -> VNode {
match self.props {
ParentVariants::Child(props) => VComp::new::<Child>(props, NodeRef::default()).into(),
ParentVariants::ChildA(props) => VComp::new::<ChildA>(props, NodeRef::default()).into(),
}
}
}
#[derive(Clone, Properties, Default, PartialEq)] #[derive(Clone, Properties, Default, PartialEq)]
pub struct ChildProperties { pub struct ChildProperties {
@ -30,6 +103,33 @@ impl Component for Child {
} }
} }
#[derive(Clone, Properties, Default, PartialEq)]
pub struct ChildAProperties {
pub string: String,
#[props(required)]
pub int: i32,
pub vec: Vec<i32>,
pub optional_callback: Option<Callback<()>>,
}
pub struct ChildA;
impl Component for ChildA {
type Message = ();
type Properties = ChildAProperties;
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
ChildA
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
unimplemented!()
}
fn view(&self) -> Html {
unimplemented!()
}
}
#[derive(Clone, Properties, Default)] #[derive(Clone, Properties, Default)]
pub struct ContainerProperties { pub struct ContainerProperties {
#[props(required)] #[props(required)]
@ -178,6 +278,22 @@ fn compile_pass() {
<ChildContainer int=1><Child int = 2 /><Child int = 2 /></ChildContainer> <ChildContainer int=1><Child int = 2 /><Child int = 2 /></ChildContainer>
</> </>
}; };
html! {
<Parent>
<ChildA int=1 />
{
html! {
<Child int=1 />
}
}
{(0..2).map(|_| {
return html! {
<Child int=1 />
}
}).collect::<Vec<VChild<Child>>>()}
</Parent>
};
} }
fn main() {} fn main() {}