-
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
Stateful views aka components support #339
Comments
Good summary of the goals of such a feature. Currently in Seed and other Elm style frameworks there are competing concerns regarding application composability, modularity and code reuse. There are many potential avenues for tackling this on a component level (including not tackling it at all). Each approach has its own advantages and disadvantages. Here are some of which I am aware of at the moment:
Each of the above has their own specific set of pros and cons. This issue is regarding (item 5) but before I write a bit more about that we need to discuss why components might be useful to have. The Elm Architecture is clearly a fantastic architecture that makes every action explicit and ultimately relatively simple to track. This is especially great for Business logic where it is essential to be able to ensure that a Domain Model is consistent and that any mutation is in response to easy to understand triggers (i.e. Messages/Commands). That said if every single action within an Elm app has to be hooked up in the same way as business logic is hooked up then boiler plate tends to grow and furthermore it becomes difficult to reuse previous code. For instance one can create a fantastic Date Picker including UI / css / etc but then to re-use it in different projects you have to clearly decide where in the model the transient state goes, which messages it gets routed through and handle any message interception to affect another part of the app. This is a lot of work just to add a self contained widget. Therefore there is some milage to exploring some of the above options. The advantage that Rust has over Elm is that it does not have to be 100% pure to be deterministic and predictable, therefore we have some wiggle room in adapting Seed's architecture to allow for components. I will reply shortly on (item 5) (React Hooks) and why this might be productive for Seed. |
React Hooks React hooks are used within React to allow for local state (amongst other features). Fundamentally the api it exposes is via The advantage of this is that all state can be made local to the view function that includes the useState(). Therefore components are trivial to create and use. Complex functionality can be created on a component basis and re-use is as simple as re-rendering the view function in another location. The are a number of drawbacks from React's implementation that includes not using useState in loops or conditionals and calling useState in the same order each invocation. Despite this React hooks are very popular and most agree that the developer experience is productive and enjoyable. Where we are now It is possible to use an almost identical api right now in Seed (albeit in nightly rust) with none of the disadvantages of React hooks implementation. Here is the code that creates a clickable counter button: use comp_state::{topo, use_state};
#[topo::nested]
fn my_button() -> Node<Msg> {
let (count, count_access) = use_state(|| 0);
div![
button![
on_click( move |_| count_access.set(count + 1)),
format!("Click Me × {}", count)
]
]
} There is zero other code or boilerplate needed for instance no fields in a model, message routing or processing in an update function. It is a self contained function that can be reused anywhere. For instance div![ my_button(), my_button(), my_button(), my_button(), my_button()] creates 5 buttons each with their own individual state and can be clicked and updated individually. The componentisation is trivial. This currently relies on the #track_caller compiler feature which is used by the topo crate to associate Ids with specific execution contexts. It might be productive for developers that something like this to be available within Seed. This issue is about exploring this possibility. |
Quick start app showing minimal example of using the |
Just an update as to where we are now. The primary api is now let val = use_state(||0) with two way binding is also implemented. I.e.: input![attrs![At::Type=>"number"], bind(At::Value, a)], |
I have no idea what am I talking about, but is it possible to use |
I don't have experience with implementing |
Update 2: rebo/seed-quickstart-hooks#5 (comment) |
Comment here rebo/seed-quickstart-hooks#6 on whether seed event handler methods on state accessors make sense for reduced boilerplate: i.e. fn my_ev_button() -> Node<Msg> {
let count_access = use_state(|| 0);
div![button![
format!("Clicked {} times", count_access.get()),
count_access.mouse_ev(Ev::Click, |count, _| *count += 1),
]]
} |
update... React's useEffect clone aka Patterns is for DOM manipulation after nodes have been rendered, or calling javascript after a component has been initially rendered. Example : fn focus_example() -> Node<Msg> {
let input = use_state(ElRef::default);
do_once(|| {
after_render(move |_| {
let input_elem: web_sys::HtmlElement = input.get().get().expect("input element");
input_elem.focus().expect("focus input");
});
});
input![el_ref(&input.get())]
} React's useEffect 'clean up' closures not implemented. |
Ok Just an update on where we all are
#[topo::nested]
fn numberbind() -> Node<Msg> {
let a = use_state(|| 0);
let b = use_state(|| 0);
div![
input![attrs![At::Type=>"number"], bind(At::Value, a)],
input![attrs![At::Type=>"number"], bind(At::Value, b)],
p![a, "+", b, "=", a + b]
]
}
A
do_once(||
call_javascript_code_highlighting_library();
).reset_on_drop();
md!(r#"
The following code's source will be highlighted:
```rust
fn seed_rocks() -> bool {
true
}
```
"#) |
Couple of tweaks to previous update.
Also e.g. in the below b and c will be unset when the component is unmounted. #[topo::nested]
fn unmount_ex() -> Node<Msg> {
let a = use_state(|| 0);
let b = use_state(|| 0).reset_on_unmount();
// longer way of saying the same thing as .reset_on_unmount();
let c = use_state(|| 0);
on_unmount(|| c.delete());
div![
format!("a:{}, b:{}, c{}",a ,b ,c ),
button![
a.mouse_ev(Ev::Click, |v| *v += 1),
b.mouse_ev(Ev::Click, |v| *v += 1),
c.mouse_ev(Ev::Click, |v| *v += 1),
"Increment"
]
]
} |
Initial draft of fetch support.
Let me know if you have any comment on the API surface. API login example as below: #[topo::nested]
// fetches a user on log-in click
fn show_user_later() -> Node<Msg> {
let fetch_user = fetch_later::<User, _>("user.json");
if let Some(user) = fetch_user.fetched_value() {
div![
"Logged in User: ",
user.name,
div![a![
attrs![At::Href=>"#logout"],
"Log out",
mouse_ev(Ev::Click, move |_| {
fetch_user.delete();
Msg::default()
})
]]
]
} else {
div![a![
attrs![At::Href=>"#login"],
"Log in",
mouse_ev(Ev::Click, move |_| {
fetch_user.do_fetch::<Model, Msg, Node<Msg>>(Msg::default());
Msg::default()
})
]]
}
} and #[topo::nested]
// fetches a user on mount
fn show_user() -> Node<Msg> {
let fetch_user = fetch_once::<User, Model, _, _>("user.json", Msg::default());
if let Some(user) = fetch_user.fetched_value() {
div![
"Logged in User: ",
user.name,
div![a![
attrs![At::Href=>"#logout"],
"Log out",
mouse_ev(Ev::Click, move |_| {
fetch_user.delete();
Msg::default()
})
]]
]
} else {
div![a![attrs![At::Href=>"#login"], "Log in"]]
}
} |
Initial draft of forms support. Chuck any struct with serde support in use_form and automatic two-way binding will be set up via With optional validation and soon nested form support. As before let me know what you think of the api surface. #[derive(Clone, Validate, Debug, Serialize, Deserialize)]
struct User {
#[validate(length(min = 1))]
name: String,
#[validate(email)]
email: String,
#[validate(range(min = 18, max = 39))]
age: u32,
}
#[topo::nested]
fn form_test() -> Node<Msg> {
let form = use_form(|| User {
name: "The Queen".to_string(),
email: "Buckingham Palace, London, England".to_string(),
age: 32,
});
div![
form.input("name"),
form.input("email"),
form.input("age"),
format!("data: {:#?}", form.get().data),
format!("errors: {:#?}", form.get().data.validate()),
]
} |
Another update on this, fully working styled components. These give the ability to define css within the component itself with no namespace issues. The issue with direct Styled components are just CSS so do not require any additional javascript or css framework. Usage is simple. The fn styled_components_example() -> Node<Msg> {
let style = use_style("color : #F00;");
div![C![style], "This is styled Component's in Seed"]
} The above simply ensures the text is red in colour. It also works well for dynamic styles , this button grows every time it is clicked: #[topo::nested]
fn button2() -> Node<Msg> {
let font_size = use_state(||24);
let style = use_style(format!(r#"
font-size: {}px;
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
outline: none;
margin: 0.5rem;
&:hover {{
background-color: bisque;
}}
"#, font_size.get() ));
button![
C![style],
format!("The font size is {}px!", font_size),
font_size.mouse_ev(
Ev::Click,|font_size,_| *font_size += 1
)
]
} |
Also included is theme support, this would enable all children components of fn view(_model: &Model) -> Node<Msg> {
let theme = Theme::new_with_styles(
&[
("primary_color", "orange"),
("secondary_color", "gray"),
]
);
use_theme(theme, ||
div![
styled_component_example(),
]
)
}
fn styled_component_example() -> Node<Msg> {
let style = use_style(r#"
font-size : 24px;
background-color : {{theme.primary_color||red}};
border-color : {{theme.secondary_color||green}};
border-width: 10px;
padding: 5px;
"#);
div![
C![style],
"This is styled Component's in Seed"
]
} Furthermore keyed animation support is present via fn styled_component_example() -> Node<Msg> {
let style = use_style(r#"
{
font-size : 20px;
border-width: 10px;
padding: 5px;
animation-name: {{anim.slide}};
animation-duration: 4s;
}
@keyframes {{anim.slide}} {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
"#);
div![
C![style],
"This is styled Component's in Seed"
]
} and media queries via {{self}}: fn styled_component_example() -> Node<Msg> {
let style= use_style(r#"
{
font-size : 20px;
border-width: 10px;
padding: 5px;
@media only screen and (max-width: 400px) {
{{self}} {
background-color: lightblue;
}
}
"#);
div![
C![style],
"This is styled Component's in Seed"
]
} |
Model
.mouse_over
forbutton
when you just want to switch the button's color on hover.StateMananger
can be extracted outside of theModel
.)Ms: Clone
.topo
), so we can fully control it and there aren't unnecessary overheads.Opinions?
The text was updated successfully, but these errors were encountered: