Ok(Blog)

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 ListItems 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 ListItems

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

part one | part three