Rust TodoMVC
Diving Deeper Into Yew
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.
TODO or Not TODO
Now that we have all the basics set up and we have Rust compiling to wasm and displaying in a browser. Let's start building a more real-world app.
First, we might as well go grab some CSS so are app can look like the TodoMVC examples. We'll be using the existing
classnames that are standard for TodoMVC and we shouldn't need to mess around with CSS ourselves. You can download the
css file for todoMVC here or you can install through npm, possibly even
find a CDN to link directly too. I just downloaded the raw file from this repo. Place the index.css
file in your static
folder and link to it
in your index.html
. I placed it in static/styles/index.css
Update index.html
<head>
/* previous code */
<link rel="stylesheet" href="styles/index.css" />
</head>
Let's update our src/app.rs
App
component to output the basic structure of our new TodoMVC app with the
proper classnames
and styling.
Let's add a new field for some state in our component and update the view
function. Leaving everything else unchanged
pub struct App {
link: ComponentLink<Self>,
item_count: u32,
}
impl Component for App {
type Message = ();
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
item_count: 0 // set initial state to 0
}
}
// previous code
fn view(&self) -> Html {
html! {
<div id="todo-app" class="todomvc-wrapper">
<section class="todoapp">
{ /* later we will add our router here */ }
<footer class="footer">
<span class="todo-count">
<strong{ self.item_count }</strong>
{ " item(s) left" }
</span>
</footer>
</section>
<footer class="info" />
</div>
}
}
}
And there we have it. If you re-build and run this you should start to see some of the styling from the classic TodoMVC
It's not much yet, but it will start to get more interesting as we go along. Before we go into building out our Router
let's
first dive into creating an Index
view. Let's create a new folder in our src
director for such components.
mkdir src/views
. And create the file touch src/views/index.rs
. As we start creating more components you will notice they basically
have the same boilerplate as our first App
Hello, Yew! example. Right now we won't have much to start with creating our Index
view. We will be iterating
on this as we go. It will serve as the basic default view in our app.
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
};
pub struct Index {
link: ComponentLink<Self>,
}
impl Component for Index {
type Message = ();
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
<>
<header class="header">
<h1>{ "todos" }</h1>
</header>
<section class="main">
<ul class="todo-list">
<li class="list-item"><label><span class="list-name">{ "Learn Rust" }</span></label></li>
<li class="list-item"><label><span class="list-name">{ "Learn Yew" }</span></label></li>
</ul>
</section>
</>
}
}
Again, very similar so far to what our original App
looked like. Notice the empty <>
and </>
. The html!
macro, similar to jsx
, should
only have one root/parent component. Since we will have a few siblings in this view, we can use the empty <>
(much like jsx
) as an empty document fragment to
act as our single parent or root. Now we can import our Index
view into our App
component.
Let's add a new mod.rs
file inside our src/views
. Here is where we will list our different modules we want to be public in our
app. Inside src/views/mod.rs
add this code
pub mod index;
pub use index::Index;
If you aren't familiar with how rust's module system works, it's generally based on the file system. There are
several ways to define how it works. By having the mod.rs
file in the src/views
folder we can declare pub mod file_name;
where file_name
would be in the current directory. Since we only have one struct
in our index.rs
file called Index
we can
reference that as index::Index
. If we had multiple structs or functions we could access them in a similar way.
Next let's add a mod views;
to our src/lib.rs
file right under mod app;
. This will make
our views
module accessible throughout our codebase. This is usually where we will add our
components
, router
, etc.
Once you have mod views;
declared in your lib.rs
let's import our use
for the Index
view in our src/app.rs
.
For now we will just add it where the router will go later.
use yew::prelude::{
// previous imports
};
use crate::views::{Index};
at the top of src/app.rs
allows use to import from our local crate the Index
view. Let's go ahead and use this view component
in our App
's view
function.
fn view(&self) -> Html {
html! {
<div id="todo-app" class="todomvc-wrapper">
<section class="todoapp">
{ /* later we will add our router here */ }
<Index />
<footer class="footer">
<span class="todo-count">
<strong{ self.item_count }</strong>
{ " item(s) left" }
</span>
</footer>
</section>
<footer class="info" />
</div>
}
}
}
We use our custom component like we would regular html <Index />
. If you are familiar with react then this should all seem familiar.
If you build and run the app now you should see our new Todo list actually has those hard coded static list items we put inside the Index
view.
However, hard-coded lists aren't going to work very well for us. Let's refactor the Index
component a little bit.
Next we are going to create a TodoList
and TodoListItem
component. Let's create a new folder for
reusable components. mkdir src/components
. You'll start to see a similar pattern here as well as we will also be
adding in a mod.rs
for our components
and all the same steps we just did for making our views
public.
You can start to organize this how ever you would like. This is just a structure that I've found to work well.
Inside our components
folder let's create another folder for our TodoList
and all the components that will go with it.
mkdir src/components/todo
. This folder will start with 2 new files.
list.rs
list_item.rs
mod.rs
Inside the mod.rs
let's publicly expose the other files we will be creating.
mod list;
mod list_item;
pub use list::List;
pub use list_item::ListItem;
Now let's go ahead and create the list.rs
file. We will be moving the current html
markup from our Index
view component
into this file. One step at a time. We'll also need the basic yew Component
code. At the top of list.rs
let's add our imports.
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
Properties,
Children,
};
Notice here we are importing two new types. Properties
and Children
. We'll see how these are used in a minute.
Continuing in list.rs
let's create a struct
for our ListProps
. We can use the Properties
type in our derive. The Properties
will allow us
to add attributes
to our struct
fields for things like default values. Some types for props it can figure out and some we need to define ourselves.
Generally when creating props for a component we will need to derive
a few things. If you forget to derive
something that a yew Component
needs the compiler will let you know. Let's add the new ListProp
struct
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct ListProps {
#[prop_or_default]
pub children: Children,
#[prop_or(String::new())]
pub class: String,
}
Below this we can create our List
struct
pub struct List {
link: ComponentLink<Self>,
props: ListProps,
}
You can call your props
field anything you like. Coming from the React world I just use props
.
Next we will implement the Component
trait.
impl Component for List {
type Message = ();
type Properties = ListProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
props,
}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props != props {
self.props = props;
true
} else {
false
}
}
fn view(&self) -> Html {
let classes = self.props.class.clone();
html! {
<ul class=format!("list {}", classes)>
<li class="list-item"><label><span class="list-name">{ "Learn Rust" }</span></label></li>
<li class="list-item"><label><span class="list-name">{ "Learn Yew" }</span></label></li>
</ul>
}
}
}
Notice this time we actually define our type Properties
as the ListProps
struct
we created.
This is basic way to define the Properties
interface for a component. It can be anything you need it to be. As of right now we still don't need it
to be for your Component
. Notice now we are giving the first parameter to the create
function a name instead of prepending it with an underscore
.
using or prepending a parameter name tells rust we know we aren't using this so don't warn us about it.
We use the type as Self::Properties
we could have also used our ListProps
type here, but I feel like Self::Properties
allows us to change and rename our
Prop Types without changing this code. Then we return Self
with the fields that are required based on how we defined our List
struct
. This is using a shorthand
since the names are the same. If we changed them we would need to define it like this.
fn create(list_props: Self::Properties, list_link: ComponentLink<Self>) -> Self {
Self {
link: list_link,
props: list_props,
}
}
But, the shorthand is more concise in this example. We add the format!("list {}", classes)
as a way to have user pass in their own classnames
through the components
class
prop if they would like. Right now we have just basically copied our ul
html markup from the original Index
component.
note If you ever have issues where a component doesn't seem to be updating it's likely because you are return false
in the update
or change
function.
Another gotcha can be in the change function, even if you are returning true
you aren't reassigning self.props = props;
to be the new props
that are passed in
so the component is still using it's original self.props
it was passed when it was initially created. The above change
function is the basic code you will almost always need
if you component should ever update when it gets new props. It's sucha a common and necessary pattern than yew
does provide additional utility function for this called neq_assign.
Let's continue our refactor by now adding the ListItem
component that we will use inside the List
component.
Same old story, the yew Component
boilerplate. Since ListItem
s will have some interaction elements, such as click. We will finally start utilizing
yew's Message
for Component
and the ComponentLink
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
Properties,
Callback,
};
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct ListItemProps {
#[prop_or(0)]
pub id: u32,
#[prop_or(String::new())]
pub item: String,
#[prop_or(false)]
pub complete: bool,
#[prop_or(String::new())]
pub class: String,
#[prop_or(Callback::noop())]
pub handle_remove: Callback<u32>,
#[prop_or(Callback::noop())]
pub handle_complete Callback<u32>,
}
pub struct ListItem {
link: ComponentLink<Self>,
props: ListItemProps,
}
pub enum ListItemMsg {
ToggleComplete,
Clicked(bool),
}
impl Component for ListItem {
type Message = ListItemMsg;
type Properties = ListItemProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
props,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
ListItemMsg::ToggleComplete => {
self.props.handle_complete.emit(self.props.id);
},
ListItemMsg::Clicked(clicked) => {
self.props.handle_remove.emit(self.props.id);
},
}
true
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props != props {
self.props = props;
true
} else {
false
}
}
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)>
<label><span class="list-item">{ item }</span></label>
</li>
}
}
}
I know we aren't currently using the interactive elements although we set the Message
and Callback
props up. We will add those shortly through some
other basic generic reusable components. It's a very common pattern to have your Message
type be an Enum
and to pattern match
it on update
.
First, let's change ListItem
to render children
instead of our static li
list markup.
We already added our children
to the ListProps
so we can just use them in the view
function.
fn view(&self) -> Html {
let classes = self.props.class.clone();
let children = self.props.children.clone();
html! {
<ul class=format!("list {}", classes)>
{ children }
</ul>
}
}
How the components will work later will be like this
<List>
<ListItem item=String::from("Learn Rust") complete=true />
<ListItem item=String::from("Learn Yew") complete=false />
</List>
But we will map over basic collection in order to dynamically generate the ListItem
s
For now let's finish this up by adding a mod.rs
to our components
folder and exposing the todo
list components.
pub mod todo;
And again at the top-level we add components
to our src/lib.rs
file right under where we added mod views;
mod app;
mod views;
mod components;
Now that the components module is exposed we can use
our new List
item in our Index
view.
Add the import under use yew::prelude::{ /* .. */ };
use crate::components::todo::{List, ListItem};
And change the view
function
fn view(&self) -> Html {
html! {
<>
<header class="header">
<h1>{ "todos" }</h1>
</header>
<section class="main">
<List class="todo-list">
<ListItem class="todo" item={ "Learn Rust" } />
<ListItem class="todo" item={ "Learn Yew" } />
</List>
</section>
</>
}
}
If you build and run the code now you should see a basic Todo list with the two items we currently still have hard coded. This will work on this in the next installment.
The final code for this part can be found on tutorial repo branch tutorial/part-two