Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature Expected: Decoupling Component #111

Closed
zengsai opened this issue May 13, 2019 · 18 comments
Closed

New Feature Expected: Decoupling Component #111

zengsai opened this issue May 13, 2019 · 18 comments

Comments

@zengsai
Copy link

zengsai commented May 13, 2019

Hi,

I'm watching seed several month. I like it and hope it become the wide-using wasm webapp dev framework.

everything is fine, except the following:

I'd like to create a frontend component library, like [ng-bootstrap](https://ng-bootstrap.github.io/#/components/alert/examples), using seed. It's a difficult task!

Is it possible that seed provide REACT like component, which have global level MODEL and component level MODEL at the same time?

Is it possible to provide a mechanism that make component can communicate with each other?

@MartinKavik
Copy link
Member

MartinKavik commented May 13, 2019

Hi,

Well, there were some flame wars in the Elm world - reusable views vs nested components. But Elm was designed mainly with reusable views in mind, so it's the recommended way of writing apps - see Elm best practices - Reusable views instead of nested components.

So if you want to write a frontend library for Seed, which shares the same architecture, I recommend you to look for patterns in Elm world.

Examples:

Personally I don't think it's possible to create React-like components in Seed and it was my reason why I've chosen Seed.

@sapir
Copy link
Contributor

sapir commented May 13, 2019

I think it is possible to do nested components, at least in some cases. Basically, you have to put the nested model inside the parent model, and pass messages back and forth.

To transform the child message type to the parent message type, I do either of two things:

  1. Have the parent pass a callback to the child, that transforms the child message to a parent message. (The callback is usually one of the constructors of the parent's message enum).
  2. Call the child component's update/view functions normally, and change the message to the parent's message type afterwards.

Then the parent has to pass any child messages it gets down to the child.

To have the child pass messages up to the parent, I have the parent intercept some of the chlid's messages. But it would probably be better to use a special callback, similar to what I described above.

So it might look something like this:

mod parent {
    use super::child;
    use seed::prelude::*;

    struct Model {
        parent_stuff: u32,
        child: child::Model,
    }

    #[derive(Clone)]
    enum Msg {
        ChildMsg(child::Msg)
        ChildToParent,
    }

    fn update(msg: Msg, model: &mut Model, orders: &mut Orders<Msg>) {
        match msg {
            Msg::ChildMsg(inner) => {
                child::update(
                    || Msg::ChildToParent,
                    Msg::ChildMsg,
                    inner,
                    &mut model.child,
                    orders,
                );
            }

            Msg::ChildToParent => {
                // do something!
            }
        }
    }

    fn view(model: &Model) -> El<Msg> {
        div![
            "This is a child component: ",
            child::view(&model.child),
        ]
    }
}

mod child {
    use seed::prelude::*;

    struct Model {
        child_stuff: String,
    }

    #[derive(Clone)]
    enum Msg {
        CheckStuff,
        DoStuff,
    }

    fn update<F1, F2, ParMs>(
        make_child_to_parent: F1,
        wrap: F2,
        msg: Msg,
        model: &mut Model,
        orders: &mut Orders<ParMs>,
    )
    where
        ParMs: Clone,
        F1: Fn() -> ParMs,
        F2: Fn(Msg) -> ParMs,
    {
        match msg {
            Msg::CheckStuff => {
                if !model.child_stuff.is_empty() {
                    orders.send_msg(wrap(Msg::DoStuff));
                }
            }

            Msg::DoStuff => {
                orders.send_msg(make_child_to_parent());
            }
        }
    }

    fn view<F, ParMs>(wrap: F, model: &Model) -> El<ParMs>
    where
        ParMs: Clone,
        F: Fn(Msg) -> ParMs,
    {
        div![
            "Hello, world! Stuff is ",
            &model.child_stuff,
            button![simple_ev(Ev::Click, wrap(Msg::CheckStuff))],
        ]
    }
}

I haven't actually tried to compile this, so maybe it doesn't even compile. But I hope it can demonstrate what I mean.

@MartinKavik
Copy link
Member

MartinKavik commented May 14, 2019

Some useful links

Good luck and keep us updated :)

@zengsai
Copy link
Author

zengsai commented May 15, 2019

Some useful links

Good luck and keep us updated :)

Thanks, I will try!

@David-OConnor
Copy link
Member

I don't have much to add to @MartinKavik and @sapir 's excellent responses, but note that in frameworks which do support stateful components, you can eschew that feature if you like. For example, in React, you can use the same patterns described above in the Elm-related links, and keep state centralized in the top-level component, or Redux store.

@tiberiusferreira
Copy link
Contributor

If I understood correctly the idea of "Reusable views instead of nested components", using Reusable views means that your "components" don't have any state.

This applied to Seed corresponds to the current "component" model which is basically a function which returns Vec<El<Msg>> as in the guide, right?

In practice this means that all state is actually stored inside the top level Model struct. While I do understand the benefits of this, this means any stateful components can't be encapsulated because it needs to "leak" its state to the top level model, right?

For example, if I wanted to create a Rich Text Editor Seed component as requested by #114 I'd probably have to keep at least some internal state (for example the cursor position, current buffer, etc), how would I do that if I couldn't keep internal state?

I'm sorry if this was already answered before, but I don't know much about Elm and wanted to see how this works out for Seed.

@David-OConnor
Copy link
Member

David-OConnor commented May 24, 2019

You're correct on all of the above.

To store cursor position etc, you'd have model fields corresponding to each of them. Perhaps a nested struct under the main model, called CursorState. The cursor component (or view func, whatever you'd like to call it) might then accept the data it needs as parameters, which would be passed down from higher-level view funcs, via the model. Let me know if an example would help.

@tiberiusferreira
Copy link
Contributor

Thanks for the quick reply. So the recommended approach would be to do something like this? :

#[derive(Clone)]
enum EditorMsgs {
    EditingBegan,
    EditingDone,
    SomeCustomMsg
}

pub struct EditorState{
    /*
    Private fields
    */
}

impl Default for EditorState{
    fn default() -> Self {
        unimplemented!()
    }
}


pub struct EditorProps{

}

impl Default for EditorProps{
    fn default() -> Self {
        unimplemented!()
    }
}

// parent to child via props which could contain callback functions
// child to parent via Msgs

pub fn rich_text_editor(state: &mut EditorState, props: &EditorProps) -> El<EditorMsgs> {
    /*Some complicated stuff*/
    div![  ]
    //.... other stuff
}

Then the user of my editor would create and store the EditorState struct and translate my EditorMsgs into its own messages, right?

@MartinKavik
Copy link
Member

It's not exactly for your use case, but there is an example of the simplest plumbing between parent and children (components) so it can be useful for you:

Note: It isn't possible to use it in Seed now, I've written some mapping functions. (It's the part of my future PR for new Fetch.)

@MartinKavik
Copy link
Member

Then the user of my editor would create and store the EditorState struct and translate my EditorMsgs > into its own messages, right?

I think you don't want to "leak" EditorMsgs into parent in your case. (Try to hide the most of Editor's types). Parent should send its own Msgs constructors into Editor and Editor will use them.
Something like standard input (copy-paste from Seed's docs):

input![ attrs!{At::Value => model.what_we_count}, input_ev(Ev::Input, Msg::ChangeWWC) ]

=> Msg::ChangeWWC is just contsructor.

I don't have exact example in Rust, but this Elm example and associated blog post should be enough for demostration:

@David-OConnor
Copy link
Member

David-OConnor commented May 25, 2019

A point to highlight from @MartinKavik 's post: While you generally pass data (Whether that's individual vars, or the whole / part of a model) down to sub-views/components, you don't need to pass up callbacks like you would in non-Redux React. Ie you can go straight to the update function via events from anywhere in the view tree.

@zengsai
Copy link
Author

zengsai commented Oct 12, 2019

fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::ExampleA(msg) => {
            example_a::update(msg, &mut model.example_a, &mut orders.proxy(Msg::ExampleA));
        }
        Msg::ExampleB(msg) => {
            example_b::update(msg, &mut model.example_b, &mut orders.proxy(Msg::ExampleB));
        }
        Msg::ExampleC(msg) => {
            example_c::update(msg, &mut model.example_c, &mut orders.proxy(Msg::ExampleC));
        }
        Msg::ExampleD(msg) => {
            example_d::update(msg, &mut model.example_d, &mut orders.proxy(Msg::ExampleD));
        }
        Msg::ExampleE(msg) => {
            example_e::update(msg, &mut model.example_e, &mut orders.proxy(Msg::ExampleE));
        }
    }
}

seams not good. any way to make it better?

@MartinKavik
Copy link
Member

seams not good. any way to make it better?

It's just an example to demonstrate one of the patterns - you don't need similar "plumbing" for small projects, basic Elm Architecture is enough.
This pattern is nice if you like explicit connections between modules and their sub-modules and don't want to accidentally use Msg which belongs to other modules. It's suitable for bigger projects with tree structure.

@rebo
Copy link
Collaborator

rebo commented Oct 12, 2019

Zengasi, what dont you like about the above pattern? It let's touch clearly separate concerns into different modules via their message types. I use this in a medium size application and it works well.

@MartinKavik
Copy link
Member

Is it possible that seed provide REACT like component, which have global level MODEL and component level MODEL at the same time?

There is no exact equivalent in Seed, but you can look at the @rebo's https://github.com/rebo/proof_of_concept_seed_hooks.


The last comment is a month without the answer so I suggest to close it.
If you have some ideas how to make Seed better, please create a proposal, ideally, with a real-world problem example.

@hwchen
Copy link

hwchen commented Nov 24, 2019

I'm a backend dev exploring Rust front-end frameworks. Of course, one of the first things I tried to do was make a component :). But reading through this issue, and reading some of the Elm discussion links helped me understand the theory of this framework.

It would have be helpful to me if this "reusable views over nested components" was mentioned explicitly in the Readme. I think it's easy to understand the basic Elm architecture pretty easily, but understanding preferred patterns for more complex apps is not obvious. As it is, I had to search several issues for the answer.

@rebo
Copy link
Collaborator

rebo commented Nov 25, 2019

There are a few decent examples about that show decoupling components by use of msg_mapper and clone_app. It would be good to have a clear and simple to follow tutorial that shows this.

The basic single message-update-view loop is fairly comprehensible to newbies but I certainly found it a bit tricky once my app size got big and I wanted to decouple/separate out concerns.

If I have time I might do a tutorial this weekend.

@MartinKavik
Copy link
Member

@rebo ad "I might do a tutorial this weekend." - I don't know if you've written something - create a new issue like "Write tutorial for bigger apps" or add a link to the complete tutorial if you want.

I'm closing this in favor of #336

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants