Rust TodoMVC
Routes and Filters
January 07, 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.
Routes as Filters
In the last part we added the rest of the functionality for completing and removing items from our todo list.
Now we will add filters for our list using the yew router. With that being said, let's go open our Cargo.toml
file and add the dependencies
for yew-router
0.14.0.
[package]
name = "yew-todo-mvc"
version = "0.1.0"
authors = ["rusttodomvc"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
yew = "0.17"
yew-router = "0.14.0"
wasm-bindgen = "0.2.67"
First let's set up a simple Enum
for our filter states inside the src/app.rs
component. We could potential put it
somewhere else, but several different components will be using this Enum
. Somewhere near the top of the src/app.rs
file
add
#[derive(Copy, Clone, PartialEq)]
pub enum AppFilter {
All,
Active,
Complete,
}
Let's create a new src/routes
folder and a new mod.rs
file there. This will be the basic top level route
definition for our app and also the first place we will use our new AppFilter
Enum
.
use yew_router::{Switch, router::Router};
use yew::prelude::{Html, html};
use crate::app::{AppFilter};
use crate::views::{Index};
#[derive(Switch, Clone)]
pub enum AppRoute {
#[to = "/complete"]
Complete,
#[to = "/active"]
Active,
#[to = "/"]
Index,
}
pub fn router() -> Html {
html! {
<Router<AppRoute, ()>
render = Router::render(|route: AppRoute| {
match route {
AppRoute::Active => html! { <Index filter=AppFilter::Active /> },
AppRoute::Complete => html! { <Index filter=AppFilter::Complete /> },
AppRoute::Index => html! { <Index filter=AppFilter::All />,
}
})
/>
}
}
This is a basic router setup using yew-router
. If you're familiar with react-router
it's familiar similiar
just more rust than javascript. Right now it looks kind of weird though? When we match
a route
we are just
return our Index
component for every route. Yes, we will always be rendering that same Index
component.
What we will use the router for will be to pass in a filter
prop to our Index
view. Our Index
view will
then use that value to filter it's items
collection based on the route. Router as state.
Let's go add the filter
prop now and also the, well, actual filtering.
In src/views/index.rs
add the import for our new AppFilter
where our other imports are and all Properties
to our yew
import like we have done with other Components
that receive props.
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
KeyboardEvent,
Properties,
};
use crate::components::{
form::Input,
todo::{List, ListItem},
};
use crate::app::AppFilter;
And under our definition for ItemData
we can add the new IndexProps
.
#[derive(Properties, Clone)]
pub struct IndexProps {
pub filter: AppFilter
}
Notice here we did not use our usual #[prop_or(Default)]
attributes for the field. This means the filter
prop is required when using the Index
view. Now we need to setup the things we always need when using Properties
for a yew component.
Our props
field on our Index
struct
pub struct Index {
link: ComponentLink<Self>,
props: IndexProps,
current_todo: String,
items: Vec<ItemData>,
internal_id: u32,
}
- Add the associated type in the
impl Component for Index
- Add
props
as input and output in thecreate
- Update the
change
function to account for theprops
impl Component for Index {
type Message = IndexMsg;
type Properties = IndexProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
props,
current_todo: String::new(),
items: Vec::new(),
internal_id: 0,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
// code stays the same as before
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props.filter != props.filter {
self.props.filter = props.filter;
true
} else {
false
}
}
fn view(&self) -> Html {
let items = self.render_items(&self.props.filter);
// rest of code stays the same as before
}
}
And finally we need to actually filter our items
which happens in our render_items
function
fn render_items(&self, filter: &AppFilter) -> Vec<Html> {
self.item.iter()
.filter(|item| {
match filter {
AppFilter::Active => !item.complete,
AppFilter::Complete => item.complete,
AppFilter::All => true,
}
})
.map(|litem| {
// map code stays the same
}).collect::Vec<Html>>()
}
Notice now our render_items
function takes in a new parameter filter
. We pass in our props.filter
and
use this to match
against items
in the collection.
We need to expose our router
module to the rest of our code in src/lib.rs
just add a mod routes
where
the other mod
declarations are.
We should head back over to our src/app.rs
and switch out the Index
view for our router
function.
use crate::routes::router;
and in the src/app.rs
view
function
fn view(&self) -> Html {
html! {
<div id="todo-app" class="todomvc-wrapper">
<section class="todoapp">
{ router() }
<footer class="footer">
<span class="todo-count">
<strong>{ self.item_count }</strong>
{ " item(s) left" }
</span>
</footer>
</section>
<footer class="info />
</div>
}
}
If you build and run this code you should at least see our Index
view still, but we have
no way yet to navigate to the routes. If you try to type the route in the address bar you will see the
list is always empty. That's because our state doesn't persist. We'll address that later using sessionStorage
.
For now, let's add our Filter
controls so we can navigate the states of the list using the router.
Let's create a new folder src/components/filter
with a mod.rs
file.
use yew::prelude::{
Component,
ComponentLink,
Html,
html,
ShouldRender,
Properties,
};
use yew_router::components::{RouterAnchor};
use crate::app::AppFilter;
use crate::routes::AppRoute;
For our imports in our Filter
you will notice we are using a new yew-router
component and the AppRoute
Enum
we created in the src/routes/mod.rs
file. Continuing on in src/components/filters/mod.rs
#[derive(Properties, Clone, PartialEq)]
pub struct FiltersProps {
#[prop_or(AppFilter::All)]
active: AppFilter,
}
pub struct Filters {
link: ComponentLink<Self>,
props: FiltersProps,
}
impl Component for Filters {
type Message = ();
type Properties = FiltersProps;
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 active = self.props.active;
html! {
<ul class="filters">
<li><RouterAnchor<AppRoute> route=AppRoute::Index>{ "All" }</RouterAnchor<AppRoute>></li>
<li><RouterAnchor<AppRoute> route=AppRoute::Active>{ "Active" }</RouterAnchor<AppRoute>></li>
<li><RouterAnchor<AppRoute> route=AppRoute::Complete>{ "Complete" }</RouterAnchor<AppRoute>></li>
</ul>
}
}
}
And that's the Filters
. Again, it's not too unlike using react-router
just more Rust-y with the types and all.
One thing you could also do to arguabley make it a bit more readable or at least less verbose is to create a type
alias for the RouterAnchor<AppRoute>
and use that instead.
// at the top-level outside your `impl Component for Filters`
type RouteFilter = RouterAnchor<AppRoute>;
And use it in your li
s instead
<li><RouteFilter route=AppRoute::Index>{ "All" }</RouteFilter></li>
Note also we use an attribute for our FilterProps
so that if we don't pass in an active
prop it will default
to our normal Index
view (unfiltered). You could also impl Default for AppFilter
in src/app.rs
and then use
the #[prop_or_default]
instead of #[prop_or(AppFilter::All)]
. It's up to you and the use-case.
Make sure to expose the Filter
in src/components/mod.rs
mod filters;
pub mod filters::Filters;
Now we need to add these controls to our app.rs
where they would normally go in the classic TodoMVC app.
Add the import at the top-level of src/app.rs
use crate::routes::router;
use crate::components::Filters;
And in the App
view
fn view(&self) -> Html {
html! {
<div id="app" class="todomv-wrapper">
<section class="todoapp">
{ router() }
<footer class="footer">
<span class="todo-count">
<strong>{ self.item_count }</strong>
{ " item(s) left" }
</span>
<Filters />
</section>
<footer class="info" />
</div>
</div>
}
}
We added our Filters
controls in the <footer class="footer">
section. With that we should have a full working
TodoMVC app built with Rust + Yew and made possibly from the amazing wasm
.
We still have a few issues. We can't reload the page or navigate away and come back. We also can't just go to a specified
route by typing in the address bar because our items
state gets wiped.
In the next part we will finish everything up and some sessionStorage
to persist the app state.
The final code for this part can be found on tutorial repo branch tutorial/part-six
part one | part two | part three | part four | part six | part seven