Rust TodoMVC
Complete and Remove
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.
Making It Even More Useful
In the last part we finished creating a working text Input
that would allow us to hit the Enter
key and
add new todo items to our list. In this section we will add functionality to our existing ListItem
controls
to remove and complete a todo item..
let's get started! First, let's add the functionality to make a ListItem
complete
. If you remember back in the previous
step we already added a struct
for ItemData
that had a field called complete
in our Index
view.
Let's add a new value to our IndexMsg
Enum
, we'll call it ToggleComplete
and let it take a u32
value, which will
be the id
of our todo item.
pub enum IndexMsg {
InputChange(String),
Keypress(u32),
ToggleComplete(u32),
}
Next, we can add to our match
arm in the update
function of our Index
Component
impl
ementation. I'm going to add
it after InputChange
and keep IndexMsg::Keypress
at the bottom. The only reason I'm doing this is because IndexMsg::Keypress
has
quite a bit more code than the other match
arms.
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
IndexMsg::InputChange(input) => {
self.current_todo = input;
},
IndexMsg::ToggleComplete(item_id) => {
for item in &mut self.items {
if item.id == item_id {
item.complete = !item.complete;
}
}
},
IndexMsg::Keypress(keycode) => {
// previous code
},
}
true
}
For the IndexMsg::ToggleComplete
match
arm we are just iterating over our self.items
collection
and checking if the item_id
from what we clicked matches an item.id
in our items
collection. If
it does we toggle the item.complete
state with the opposite of what it was previously (!item.complete
).
By using a toggle instead of just assigning item.complete = true;
we can not only mark todo items as complete
but we can reverse that and take them back to a not complete state.
Finally we can change our regular Index
impl
ementation's render_items
function to pass our ListItem
a link.callback
for the handle_complete
prop.
fn render_items(&self) -> Vec<Html> {
self.items.iter()
.map(|litem| {
let ItemData { name, id, complete } = litem;
html! {
<ListItem
key={ *id as i128 }
id=id
class="todo"
item=name
complete=complete
handle_complete=self.link.callback(IndexMsg::ToggleComplete)
/>
}
}).collect::<Vec<Html>>()
}
And there you have it. If we build and run now we should be able to toggle the complete
state of each item in our
list. If the CSS and class
names have been setup, when the state goes to complete
you should see a strikethru
like
styling over any items marked as complete
. If you inspect the elements in your browser you should see as you click on the
item's circle Checkbox
it adds or removes a complete
class
name on the element.
Removing Items
Next, let's add to the functionality and make it so we can click that little X button and remove items that
we no longer need to do, but never really completed either. It will be nearly the same code in the same places
that we just modified for the complete
functionality.
Add a new RemoveItem
to our IndexMsg
Enum
.
pub enum IndexMsg {
InputChange(String),
Keypress(u32),
ToggleComplete(u32),
RemoveItem(u32),
}
Moving on to the update
function and adding a new match
arm.
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
IndexMsg::InputChange(input) => {
self.current_todo = input;
},
IndexMsg::ToggleComplete(item_id) => {
// previous code
},
IndexMsg::RemoveItem(item_id) => {
self.items.retain(|item| item.id != item_id);
},
IndexMsg::Keypress(keycode) => {
// previous code
},
}
true
}
We are using rust's Vec
retain
here. It retains only the elements specified by the predicate. It operates in place,
visiting each element exactly once in the original order and preserves the order of retained elements.
So we retain
any item in the items
Vec
if it's id
does not match the item_id
we are being passed when we match the IndexMsg::RemoveItem
Message
.
Speaking of order. Let's go on ahead and derive
a few things for handling ordering of elements for our original ItemData
struct
and some for comparing them.
#[derive(Ord, PartialOrd, Eq, PartialEq)]
struct ItemData {
pub id: u32,
pub name: String,
pub complete: bool,
}
We didn't modify anything inside the struct
we just derive
d a few Trait
s to be applied to our ItemData
.
You can read more in depth about exactly how these Trait
s from the std
library work, if you'd like.
note one nice thing about rust is that it's ordering of structs can be quite complex without doing any
work ourself. Since the first field of our struct, id
, is a number then rust will order the ItemData
by that number. If
for some reason we had two ItemData
structs that had the same id it would move to the next field, name
, which is a String
and order by that alphabetically and on and on for each field. So the order you write the fields can be important.
You can also do you own impl Ord for YourStrict
if you need even more complex comparisions for sorting
We'll actually be adding a few more later on. For now, let's continue with updating our render_items
function
and add our handle_remove
prop with a link.callback
fn render_items(&self) -> Vec<Html> {
self.items.iter()
.map(|litem| {
let ItemData { name, id, complete } = litem;
html! {
<ListItem
key={ *id as i128 }
id=id
class="todo"
item=name
complete=complete
handle_complete=self.link.callback(IndexMsg::ToggleComplete)
handle_remove=self.link.callback(IndexMsg::RemoveItem)
/>
}
}).collect::<Vec<Html>>()
}
If we build and run we should be able to hover over a ListItem
to see the X button on the right hand side.
Clicking the X button now should remove the item from our todo list.
We just about have all the functionality we need. In the next lesson we will add a Router
to act as
a type of filter for our different list states (all, completed, active).
The final code for this part can be found on tutorial repo branch tutorial/part-five
part one | part two | part three | part four | part six