-
Notifications
You must be signed in to change notification settings - Fork 155
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
Comments
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. |
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:
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. |
Some useful links
Good luck and keep us updated :) |
Thanks, I will try! |
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. |
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 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. |
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 |
Thanks for the quick reply. So the recommended approach would be to do something like this? :
Then the user of my editor would create and store the EditorState struct and translate my EditorMsgs into its own messages, right? |
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.) |
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.
=> I don't have exact example in Rust, but this Elm example and associated blog post should be enough for demostration: |
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 |
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? |
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. |
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. |
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. |
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. |
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. |
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?
The text was updated successfully, but these errors were encountered: