Ok(Blog)

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 implementation. 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 implementation'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 derived a few Traits to be applied to our ItemData.

You can read more in depth about exactly how these Traits 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