Yew: Components should be able to wrap html elements in the `html!` macro

Created on 24 Jul 2019  路  32Comments  路  Source: yewstack/yew

Description

The html! macro should allow components to enclose other elements and have that be set as the children property in the component's props. This is allowed in JSX as seen here: https://reactjs.org/docs/composition-vs-inheritance.html

Expected Results

This should compile

html! {
  <Dialog>
    <div class="header"> ... </div>
    <div class="body"> ... </div>
  </Dialog>
}

Most helpful comment

@krankur it will handle nested components as well 馃憤

All 32 comments

Does this issue also cover nested components or just plain HTML elements? By nested components I mean something like:

<Dialog>
    <DatePicker></DatePicker>
    <div>...</div>
</Dialog>

where DatePicker is a component nested within Dialog component.

@krankur it will handle nested components as well 馃憤

Will there be a way to access the state of the nested components, or will we be given just plain VNodes as properties?

I'd like to create a routing solution for Yew (akin to something like React Router) and would like to express it like so:

<Router>
    <Route<MyComponent>: path="/path/to/my/component" />
</Router>

Skimming the source for vcomp, I don't see an easy way to allow access to child component's internal state from a parent (needed for getting routing info).

@hgzimmerman great point, I will play around with the code and see what will work well for children access

@hgzimmerman and @krankur would you mind playing with this wip branch: https://github.com/jstarry/yew/tree/component-children and let me know any issues you may have with it? Check out the nested_components example for usage

Just going by what I can see in the provided example right now, I won't have a chance to play with it for a few hours.

It's great that you already have support for nesting this quickly.

What I can't tell without playing with it, is if you can specify multiple children at once and hand them to the props as a Vec<Box<Trait>>.

I also need to experiment and see if the aforementioned Trait can be something other than Renderable<T>, because if that is allowed, then arbitrary access to child component state would be possible, which is what I would need to create a Router without extraneous syntax quirks for end-users. Without looking at the implementation yet, I have to imagine a whole bunch of unsafe nonsense would have to be used to cast/filter arbitrary vnode items into a user-specified Vec of trait objects.

I'll take a closer look later.

Ok, looking at the source for current approach, I can see that you collect all the children into a Vec and then create a Boxed Fn returning a VList of these children. This with the blanket impl for Renderable<T> over Fn() -> Html<T> is what allows the children to be presented as a Box<dyn Renderable<T>> in props.

Now that I have a slightly better understanding of what is going on, do you think instead of encapsulating the children in a Box<dyn Fn() -> Html<T>>, you could present the children using something like Vec<Box<dyn Any + Renderable<T>> and implement Renderable for that? That might allow people like me to dynamically downcast_ref() children to a trait object they define themselves to allow inspection of the child objects? (EDIT: This still leaves out the detail on how to get a reference to a plain Component from an arbitrary VNode).

As a disclaimer, I've never used any of this dynamic typing stuff within Rust, so I can't comment on if it is feasible or not, just that it might be an interesting option to explore.

Thanks for your work on this thusfar!

Dumb workaround I made.

First need an intermediate trait, to help implement Clone on property types. Can't do Box<Fn() -> HTML<COMP> + Clone> since a) Clone isn't an auto trait and b) the return value of Clone for a dynamic trait would be unknown sized. This trait adds a dynamic method to clone into a box. (Could also just wrap the Fn in an Rc probably)

pub trait HtmlClosure<COMP: yew::Component>: Fn() -> Html<COMP> {
    fn boxed_clone(&self) -> Box<dyn HtmlClosure<COMP>>;
}
impl<COMP: yew::Component, T: Fn() -> Html<COMP> + Clone + 'static> HtmlClosure<COMP> for T {
    fn boxed_clone(&self) -> Box<dyn HtmlClosure<COMP>> {
        Box::new(self.clone()) as Box<dyn HtmlClosure<COMP>>
    }
}

Usage in a properties element (need to manually implement traits for stuff):

pub struct LinkProps {
    pub path: String,
    pub inner_html:  Box<dyn HtmlClosure<Link>>,
}
impl Clone for LinkProps {
    fn clone(&self) -> Self {
        Self {
            path: self.path.clone(),
            inner_html: self.inner_html.boxed_clone(),
        }
    }
}
impl PartialEq for LinkProps {
    fn eq(&self, other: &Self) -> bool {
        return self.path == other.path &&
            &*self.inner_html as *const _ == &*other.inner_html as *const _;
    }
}
impl Default for LinkProps {
    fn default() -> Self {
        Self {
            path: "".into(),
            inner_html: Box::new(|| html!{<></>}),
        }
    }
}

In render, just call the function and splice in the result. Ex: html!{<a>{(self.props.inner_html)()</a>}

Tag usage:

<router::Link path="/1" inner_html=Box::new(|| html!{ <>{"Link 1"}</> })/>

@ColonelThirtyTwo nice workaround! Funnily enough I finished my implementation the same time you posted yours 馃槈 I was running into similar issues with needing to have custom implementations for Clone and other traits but after much head scratching found a simpler approach 馃槄Check out https://github.com/yewstack/yew/pull/589 if you're curious!

@hgzimmerman can you take a look again at the PR? You can now access children props and can filter out children when rendering.

Relevant example code:
https://github.com/yewstack/yew/pull/589/files#diff-cc4e82debe36efce98c787323a482cadR24
https://github.com/yewstack/yew/pull/589/files#diff-bcced8558d547228999cca79a521fa7bR49

Wow, this looks great. I've been working on a macro that will create a route matcher from a provided string and then using the result of the matcher to construct props for a component, and something like this is the final piece of the puzzle to get it looking really similar to ReactRouter. I'll hopefully be using this within a couple of days (and providing more detailed feedback), and soon after that adding a peripheral piece of Yew infrastructure to the stack.

Thanks!

Hi everybody,
I am completely new in yew / rust. How can I join this branch tests? I'm trying to implement the bootstrap library as the first project, highly inspired by reactstrap (I didn't see any solution on crates.io) - It would be really nice to have feature in container/row/column implementation. I was really worried if nesting components will be adapted (read README and docs twice).
Greets

Hey @gmorus, you can tryout the PR by changing your Cargo.toml from:

yew = "0.8"

to:

yew = { git = "https://github.com/jstarry/yew", branch = "component-children" }

@jstarry Thank you so much :)

@jstarry
I apologize in advance for dummy questions :innocent:

Is there any possibility for typing children as any proper yew/html component?

I mean sth like React/TS:

interface Props {
  children?: React.ReactNode
}

I followed your examples and wrote sth like this:

// ...
use crate::navbar::Navbar;


type Children<T> = Box<dyn Fn() -> Vec<VChild<T, Container>>>;

#[derive(Properties)]
pub struct Props {
    pub fluid: bool,
    pub class: String,
    #[props(required)]
    pub children: Children<Navbar>,
}

pub struct Container {
    props: Props,
}
// ...
impl Renderable<Container> for Container {
    fn view(&self) -> Html<Self> {
        html! {
            <div class="container">
            { for (self.props.children)().into_iter() }
            </div>
        }
    }
}
// ...

and

// ...
impl Renderable<App> for App {
    fn view(&self) -> Html<Self> {
        html! {
            <>
                <Navbar value="App" />
                <Container>
                    <Navbar />
                    <Navbar />
                </Container>
            </>
        }
    }
}
// ...

but trying to figure out reusable solution.
I think that the case is typing but not so fluent to change it properly.

Thanks for any tips.

@gmorus yeah I plan to have nicer types before merging that PR, thanks for your feedback! If you have any suggestions, please share, thanks!

@jstarry my pleasure. My repository is available here. I wanna create new group and new repo for this project on github and publish as a crate on crates.io but i dont feel so confident at rust/yew yet. Any CR or tips will be appreciated.

So now I will try to implement my internal library components reusable typing and then I will merge them with yew build-in typings.

@jstarry Hi, I'm still trying to develop some components based on your component-children feature branch. As I said, I am really baby rust dev so many concepts are very new for me, but its a list of my main problems while writing code (i can't figure out how to achieve this functionality - I think its mainly caused by typing problems):

  • pure HTML nesting
  • HTML elements typings
  • any component nesting (i mean component/html)
  • optional children prop pass - after removing #[props(required)] i got following compiler errors:
   Compiling yewstrap v0.1.0 (/home/dobrykurs/Projects/yewstrap)
error[E0277]: the trait bound `dyn std::ops::Fn() -> std::vec::Vec<yew::virtual_dom::vcomp::VChild<components::row::Row, components::container::Container>>: std::default::Default` is not satisfied
  --> src/components/container.rs:10:10
   |
10 | #[derive(Properties)]
   |          ^^^^^^^^^^ the trait `std::default::Default` is not implemented for `dyn std::ops::Fn() -> std::vec::Vec<yew::virtual_dom::vcomp::VChild<components::row::Row, components::container::Container>>`
   |
   = note: required because of the requirements on the impl of `std::default::Default` for `std::boxed::Box<dyn std::ops::Fn() -> std::vec::Vec<yew::virtual_dom::vcomp::VChild<components::row::Row, components::container::Container>>>`
   = note: required by `std::default::Default::default`

error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: Could not compile `yewstrap`.

To learn more, run the command again with --verbose.
error: build failed

If u know how to workaround any of these issues i will be glad to know ;)

I also created repo for this project (first dev stage functionality on develop branch): Yewstrap (example usage here)

Thanks for support

@gmorus When you fail to pass a prop in a component, and it isn't annotated as required, it calls Default::default() to get a value to stick there. Under typical situations this is resolved by either implementing Default for your type (which you probably shouldn't do for a boxed Fn, or wrapping it in an Option (which I haven't tried in the context of getting component children yet, and very well may not have support).

@jstarry I'd like to report that I've ran into a problem. The following (simplified) code fails to compile:

let path = path!("/a/{}");
html!{
    <Router>
        <RouteChild path=path target=Box::new(<<AModel as Component>::Properties> as FromMatches::from_matches) />
    </Router>
}

with error:

no method named `build` found for type `yew_router::router::RouteChildPropsBuilder<yew_router::router::RouteChildPropsBuilderStep_missing_required_prop_path>

This strikes me as unusual, as I am providing a path in the RouteChild.

Relevant code is available here:
https://github.com/hgzimmerman/YewRouter/blob/nested_router_stuck/YewRouter/examples/routing_component/src/main.rs#L75
https://github.com/hgzimmerman/YewRouter/blob/nested_router_stuck/YewRouter/src/component_router/router.rs#L202


Unrelated, but I couldn't seem to get this version of Yew's html!{} macro to handle components with type parameters (eg <Router<()>>). I don't recall this being a problem in the past, but I've had to remove type parameters from my router to avoid expected valid html element errors.

@hgzimmerman Thank you for your reply. Unfortunately, most of the concepts you mentioned are so new to me that trying to improve them in the context of your frameworks is like a wandering in the fog. Of course, I'll look at your tips, but I'm afraid I'm not yet qualified :cry:

For now, I can only improve my skills and wait for some new examples :rofl:

@gmorus Sorry for any ambiguity. What I meant to communicate is that the following might work:

//! container.rs
type Children<T> = Box<dyn Fn() -> Vec<VChild<T, Container>>>;

#[derive(Properties)]
pub struct Props {
    pub fluid: bool,
    pub class: String,
    pub children: Option<Children<Row>>, // An Option<> was added here
}

Because this feature is still in development, and I haven't taken the time to look at jstarry's macro magic, I don't know if what I suggest will work, but is probably your best bet for now until further developments.

@hgzimmerman I tried this solution but still cannot figure out how to make it working. I changed Props typing like in an example above but that causes following error:

   Compiling yewstrap v0.1.0 (/home/dobrykurs/Projects/yewstrap)
error[E0618]: expected function, found enum variant `self.props.children`
  --> src/components/container.rs:62:23
   |
62 |                 { for (self.props.children)().into_iter() }
   |                       ^^^^^^^^^^^^^^^^^^^^^--
   |                       |
   |                       call expression requires function
help: `self.props.children` is a unit variant, you need to write it without the parenthesis
   |
62 |                 { for self.props.children.into_iter() }
   |                       ^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error
For more information about this error, try `rustc --explain E0618`.
error: Could not compile `yewstrap`.

To learn more, run the command again with --verbose.
error: build failed

I tried to follow rust docs examples and rewrote returned value to:

html! {
    <div class={classes}>
        // { for (self.props.children)().into_iter() }
        { 
            match self.props.children {
                Some(ref children) => for (children)().into_iter() ,
                None => {},
            }
        }
    </div>
}

but got new ones:

   Compiling yewstrap v0.1.0 (/home/dobrykurs/Projects/yewstrap)
error: missing `in` in `for` loop
  --> src/components/container.rs:58:61
   |
58 |                         Some(ref children) => for (children)().into_iter() ,
   |                                                             ^ help: try adding `in` here

error: expected one of `.`, `?`, `{`, or an operator, found `,`
  --> src/components/container.rs:58:76
   |
58 |                         Some(ref children) => for (children)().into_iter() ,
   |                                            --                              ^ expected one of `.`, `?`, `{`, or an operator here
   |                                            |
   |                                            while parsing the `match` arm starting here

warning: unreachable expression
  --> src/components/container.rs:53:9
   |
53 | /         html! {
54 | |             <div class={classes}>
55 | |                 // { for (self.props.children)().into_iter() }
56 | |                 { 
...  |
62 | |             </div>
63 | |         }
   | |_________^
   |
   = note: #[warn(unreachable_code)] on by default
   = note: this warning originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

error[E0277]: `()` doesn't implement `std::fmt::Display`
  --> src/components/container.rs:53:9
   |
53 | /         html! {
54 | |             <div class={classes}>
55 | |                 // { for (self.props.children)().into_iter() }
56 | |                 { 
...  |
62 | |             </div>
63 | |         }
   | |_________^ `()` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `()`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: required because of the requirements on the impl of `std::string::ToString` for `()`
   = note: required because of the requirements on the impl of `std::convert::From<()>` for `yew::virtual_dom::vnode::VNode<components::container::Container>`
   = note: required by `std::convert::From::from`
   = 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: aborting due to 3 previous errors
For more information about this error, try `rustc --explain E0277`.
error: Could not compile `yewstrap`.

To learn more, run the command again with --verbose.
error: build failed

Lets say optional children props is not my highest priority now and it exceeds my knowledge so Ill try to implement some more basic stuff now and improve my skills meanwhile ;) Thank you for tips.

@gmorus children can now be optional 馃憤 Run cargo update -p yew to get the latest changes. Also take a look at this commit to see how the example changed: https://github.com/yewstack/yew/pull/589/commits/a6c88a139e99d949816ae75372920054fe02e304#diff-11b3515847558c7863f5aff2c0a9fc05

@hgzimmerman working on a fix for generic components, sorry about that!

Thanks a ton! The turnaround time on that was quick.

While what I had still confused the html! macro, removing the target prop allowed it to parse correctly again. Maybe the multiple casting to traits I had created a string of tokens the macro couldn't adequately deal with. Its such a niche problem that it probably isn't worth dealing with right now. It doesn't bother me at all because I was already planing on moving the creation of the "target" boxed Fn into the PathMatcher returned by the route!() macro instead of passing it directly via props.

Maybe keep in the absolute back of your mind that something (probably the < characters ) present in:

target=Box::new(<<AModel as Component>::Properties> as FromMatches::from_matches)

can cause the parser to fail to parse any props and report a misleading error message.

@jstarry I did the first tests of the new component-children feature version. Optional children props works fine, required children pros still working as expected. Nesting components and plain HTML works properly. I really appreciate your work, good job!

I was able to achieve following <App /> shape:

impl Renderable<App> for App {
    fn view(&self) -> Html<Self> {
        html! {
            <div>
                <Navbar title="Yewstrap app" />
                <Container fluid=true>
                    <Row class="xxx">
                    </Row>
                </Container>
                <Container fluid=false>
                    <Row class="xxx">
                    </Row>
                    <Row class="vvv">
                        <Col class="ttt"/>
                    </Row>
                </Container>
                <Container class="container-main">
                    <Row>
                        <Col>
                            <h1>{"Test header"}</h1>
                            <p>
                                <h2>{"Second header test"}</h2>
                                {"Test paragraph"}
                                <Container />
                            </p>
                        </Col>
                    </Row>
                </Container>
            </div>
        }
    }
}

Edit: I removed section about forcing the same type of component children as parents. I missed the difference between Children and ChildrenWithProps, my mistake.

Other nice-to-have feature would be possibility of declaring multiple allowed children component types and mixing it with HTML elements types.

Thanks again for your work!

@hgzimmerman I pushed a new change on that branch which fixes generics parsing, I'll look into that parser bug too, thanks!

@gmorus I pushed a new syntax for working with children, can you try it out and let me know what you think? https://github.com/yewstack/yew/pull/589/commits/da506e2343cac6a0105bb0c817a33e36a617958f#diff-11b3515847558c7863f5aff2c0a9fc05

I realised the Children<T> type will be most useful in most cases in my library, and it works fine. I also tried new syntax for ChildrenWithProps<C,P> - also looks fine. I really love React-like elements mapping. It would be also so much useful in future to implement similar solution with Children<T> children prop type.

@jstarry I also suggest add ChildrenWithProps<C,P> to yew::prelude; - it may be useful there.

I'd just like to let you know that thanks to this branch, I've been able to get a working prototype of the router completed. Its not entirely done, as there are likely syntax changes coming to the route! macro I've made, error reporting needs to be improved, and possibly some orphaning bugs in yew to work out, but the external interface is more or less stable.

Here's a sample of what it looks like for the moment:

html!{
    <Router>
        <Route path=route!("/a/{}" => AModel) />
        <Route path=route!("/c" => CModel) />
        <Route path=route!("/b/{sub_path}" => BModel) />
    </Router>
}

Thanks a bunch for enabling this @jstarry

@hgzimmerman woot! That's awesome! Can't wait to try this out. Will it be possible to pass in props to the components in the routes? Something like this?

html!{
    <Router>
        <Route path="/a/{}" >
            <AModel prop=value />
        </Route>
    </Router>
}

@jstarry I hadn't planned on supporting something like that. I rely on a pair of macros to create a path matcher that can capture sections of a path and turn them into props for the target component. It relies on the consumer of the library to make sure that the names in the capture groups match those of the props's fields for the component that will be rendered, and that the field's types can be converted to from &str. There sadly isn't a way to validate that at compile-time (at least using my current approach), but it is possible to run a derived verification function that will panic immediately at runtime, which is better than nothing.

The problem I see with supporting having arbitrary children inside of <Route>...</Route is that there isn't a way to pass the contents of the matched section of the url to the component.

~~I would like to support nested route targets, but I think doing so requires giving up the direct capturing of prop values from the URL. ~~

I'm open to feedback and discussion, and would appreciate your input, but I don't think this thread is the most appropriate place for it. I've opened a issue in case you want to continue discussion there.

EDIT: Yes, now it is possible to render children inside of Route, and you can pass in sections captured in the url in either FromMatches implementations (same as before), as well in render functions that can support arbitrary html elements.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sackery picture sackery  路  3Comments

zethra picture zethra  路  5Comments

alun picture alun  路  4Comments

kellytk picture kellytk  路  3Comments

sanpii picture sanpii  路  3Comments