Rust TodoMVC
Iterating and Iterating
January 04, 2021
This tutorial will guide you through the basics of using Rust along with wasm and Yew to build a basic TodoMVC web app. Building on this in later tutorials to start building more complex web apps utilizing Rust for both the client and server.
Bringing It Together
In this part of the tutorial we will be creating more smaller reusable components and composing them with some of the previous components we made to make more complex components with some interactive functionality.
Right now we have a simple list of items that are just text. But we can't add, complete or edit any of our
todo items. Our ListItem
component needs some new functionality. Let's add a Checkbox
input and a Button
We'll start with the button because it's a little simpler.
We can create a button.rs
file in the src/components
folder and, you guessed it, add the yew Component
boilerplate.
You may want to create a snippet or recording of the boilerplate so that you can easily click a button or two and auto-populate
new component files. The button we are going to make will be pretty simple and very generic. You could use this anywhere you need
a button. Not just in this context.
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
Properties,
Children,
Callback,
};
#[derive(Properties, Clone)]
pub struct ButtonProps {
#[prop_or_default]
pub children: Children,
#[prop_or(String::new())]
pub class: String,
#[prop_or(Callback::noop())]
pub handle_click: Callback<bool>,
}
pub struct Button {
link: ComponentLink<Self>,
props: ButtonProps,
}
pub enum ButtonMsg {
Clicked,
}
impl Component for Button {
type Message = ButtonMsg;
type Properties = ButtonProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
props,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
ButtonMsg::Clicked => {
self.props.handle_click.emit(true);
},
}
true
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
let classes = self.props.class.clone();
let children = self.props.children.clone();
html! {
<button
class=format!("btn {}", classes)
onclick=self.link.callback(|_| ButtonMsg::Clicked)
>
{ children }
</button>
}
}
}
And there we have a simple generic button that we can use anywhere we need a button. We can iterate on this
at any point in the future if we need. Or compose more specific buttons from this button.
Notice we use children
. This allows us to use something as simple as text for the button or anything as
complex as more html!
or another Component
.
The main thing here is that we are now using the onclick
property to create an event listener.
Unfortunately as of yet, yew does not delegate events for you so you could end up with an extreme amount of event listeners if your
app is large enough. The basic usage in rust is to use the link
's callback
function to "send" your message.
When self.link.callback
your component update
function will receive the message. In the update
function
will receive the message (msg
) argument and then we can match on the enum
that we declared as our Component
's Message
type.
In the update
function when we match on ButtonMsg::Clicked
we call the user provided handler Callback
that we get passed in as a prop.
Defined in our ButtonProps
. A Callback
in yew will have an emit
function on it and calling this is how we can run the callback.
Since we are passing this callback in as the prop handle_click
the parent Component
that uses our Button
will be notified of when the button
is clicked and can handle it however they need.
Let's go add a remove button to our ListItem
to see how it works together. Before we do that we need to
expose the Button
component. In src/components/mod.rs
add
mod button;
pub use button::Button;
Some of the mod/use may seem redundant, but it helps to make the public api of our lib actually less verbose for the user.
Now we can head on over to the src/components/todo/list_item.rs
and update it to use our new Button
components.
Of course, add our import near the top of the file.
use crate::components::Button;
In the previous section of our tutorial we had already setup some of the code that will make this button work in our ListItem
component.
Let's finish it up for the remove button. We should actually only need to update the view
function, but feel free to look at the update
function and
the ListItemMsg
enum we had created before.
fn view(&self) -> Html {
let id = self.props.id;
let item = self.props.item.clone();
let classes = self.props.class.clone();
let completed = if self.props.completed { "completed" } else { "" };
html! {
<li class=format!("list-item {} {}", classes, completed)>
<div class="view" id=id>
<label><span class="list-name">{ item }</span></label>
<Button class="destroy" handle_click=self.link.callback(ListItemMsg::Clicked) />
</div>
</li>
}
}
There you have it. Now when the button is clicked in our ListItem
it will "handle click" which calls
the self.link.callback
using the ListItemMsg::Clicked
enum. So that our ListItem
can decide what to
do when a button is clicked on it. If you look at what we previously had it's fairly straight forward.
ListItemMsg::Clicked(clicked) => {
self.props.handle_remove.emit(self.props.id);
}
Again, we let the parent component handle what exactly it wants to do. We just call the handler handle_remove
that we were passed as a prop with the
value of our self.props.id
. This may seem a bit convoluted to keep passing off what we do to the parent component, but it makes it easier to make generic
reusable components that can be composed into more complex components and functionality. What we are going for here is that whatever parent component
is in charge of updating the actual list data will know that handle_remove
was called so it should remove an item from the data and it gets the id of the
item that needs to be removed so it can identify which item it should delete or filter.
This should become more clear as we work our way up the component tree to the top level.
For now, let's go ahead and create the final piece of our ListItem
component. The Checkbox
that allows us to
toggle whether or not our todo item is completed.
Since it is a checkbox let's create a src/components/form
folder where we can group other form related
components such as text inputs together. And then a new file here called checkbox.rs
.
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
Properties,
Callback,
};
#[derive(Properties, Clone, PartialEq)]
pub struct CheckboxProps {
#[prop_or(String::new())]
pub value: String,
#[prop_or(String::new())]
pub placeholder: String,
#[prop_or(String::new())]
pub class: String,
#[prop_or(false)]
pub checked: bool,
#[prop_or(Callback::noop())]
pub handle_change: Callback<bool>,
}
pub struct Checkbox {
link: ComponentLink<Self>,
props: CheckboxProps,
checked: bool,
}
pub enum CheckboxMsg {
ToggleChecked,
}
impl Component for Checkbox {
type Message = CheckboxMsg;
type Properties = CheckboxProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let checked = props.checked;
Self {
link,
props,
checked,
}
}
}
By now I'm sure you really have seen the pattern here in setting up yew components. It's generally
the same basic thing. However, notice here we are adding a little extra state and setting it on create
.
This is for the checkbox's "checked" state. Since we derived Properties
for the CheckboxProps
and gave
this checked
field a default of false
it makes this prop optional to the user to supply it. It will
be created in it's unchecked state. Unless the user passes true
for the checked
prop. Moving along
we will see the update
, change
, and view
functions are fairly simple and standard as well. Let's
add them after the create
as we normally would.
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
CheckboxMsg::ToggleChecked => {
self.checked = !self.checked;
self.props.handle_change.emit(self.checked);
},
}
true
}
Here we again match
the msg
on we toggle the state self.checked
with the opposite of whatever it was
before we checked it by flipping the bool
value. In our view
we will simple add a onclick
handler that will
send our CheckboxMsg::ToggleChecked
to the update
. Notice here we also call the handle_change
prop the user has supplied
us with the current value of whether or not we are checked. For now this will do, we may want to later also send the value
of the checkbox or other additional data.
Moving on to our change, it's the same standard one as usual.
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props != props {
self.props = props;
true
} else {
false
}
}
And our view
is also very simple. We will simple use an html
input
with a type
of checkbox
fn view(&self) -> Html {
let value = self.props.value.clone();
let classes = self.props.class.clone();
html! {
<input
type="checkbox"
class=classes
onclick=self.link.callback(|_| CheckboxMsg::ToggleChecked)
checked={ self.checked }
value=value
/>
}
}
The main 2 things to notice here are the onclick
as I described above and the checked
prop of the input
.
We are using are self.checked
state here that we manage in the component after it is initially created. Not
the self.props.checked
prop that the user passes in. After the component is create
ed we no longer let
them control that part of the internal state.
Now we need to add those to our mod.rs
files both in src/components
pub mod form;
and in src/components/form/mod.rs
which will be a new file.
mod checkbox;
pub use checkbox::Checkbox;
If you want you can just have pub mod checkbox;
here. Then you would import that component in this way
use crate::components::form::checkbox::Checkbox;
Instead of
use crate::components::form::Checkbox;
Again it seems verbose or redundant in our mod.rs
file, but less so for when using it, but it's up to you how you
would like to expose your modules.
Now that we have the final interactive piece of our ListItem
puzzle let's go ahead and use it now.
Right under our Button
import we can add our new Checkbox
use crate::components::form::Checkbox;
And again, like we had for the Button
we already had the message and update
functionality in place.
We can just edit our view
to use the new Checkbox
.
fn view(&self) -> Html {
let id = self.props.id;
let item = self.props.item.clone();
let classes = self.props.class.clone();
let completed = if self.props.complete { "completed" } else { "" };
html! {
<li class=format!("list-item {} {}", classes, completed)>
<div class="view" id=id>
<Checkbox
class="toggle"
value=item.clone()
checked=self.props.complete
handle_change=self.link.callback(|_| ListItemMsg::ToggleComplete)
/>
<label><span class="list-name">{ item }</span></lable>
<Button class="destroy" handle_click=self.link.callback(ListItemMsg::Clicked) />
</div>
</li>
}
}
And there we have our complete ListItem
component. We'll still need add functionality in our parent component (Index
) that will be managing
the items
data, but if you build and run this it should start looking more like the classic TodoMVC example.
The checkboxes internal state should be reflected and you'll see the X for the remove button when hovering over an item.
We're getting closer to having a working TodoMVC list.
The final code for this part can be found on tutorial repo branch tutorial/part-three
In the next part we will continue making the list dynamic by adding a way to create new todo items with an input.