WebAssembly Rust front-end with Yew: how to P1

In this tutorial, we will learn some basics on how to build a web front-end using pure Rust compiled to WebAssembly with the help of the Yew framework. This is part 1 of a multi-part series exploring various concepts within the Yew framework. In this first part, we will look at how to generate HTML with Yew, what components are, some event handling, and manipulating HTML elements.

After we have finished the project explained in this tutorial we will have experience with:

  • Creating components
  • Handling events
  • Manipulating element values

The complete code for this tutorial can be found on my GitHub: https://github.com/tmsdev82/yew-basic-web-app

Part two of the tutorial is here: CSS and JavaScript WASM Rust: Yew how to P2.

Contents

Rust, WebAssembly, and Yew

Before we begin working on the project, some background information on WebAssembly. Because how is it that we can use Rust to create a web front-end anyway? That is thanks to WebAssembly or WASM for short.

What is WebAssembly?

WASM is a binary instruction format for a stack-based virtual machine. This means that if you can compile a program to these this WASM binary instruction format, this virtual machine will be able to execute it.

Rust can easily be compiled into WASM, as there is very good tooling available to help us with that. For this tutorial, we will use Trunk.

The four most popular browsers (Firefox, Chrome, Safari, and Edge) support executing WebAssembly. So one of the primary uses of WASM is running code in a browser. However, WASM’s use case is not limited to just Web Applications running in a browser.

Why is WebAssembly interesting? Well, the code is supposed to run at “native speeds”, meaning it has the potential to run faster than JavaScript.

For more information on WebAssembly in general please see: https://webassembly.org/

What is Yew?

Yew is a Rust framework for creating front-end web applications using WebAssembly. With Yew, we can do basically anything web front-end related, but instead of using JavaScript for functions, calling backend APIs, state management, etc, we can write Rust code.

Yew is a component-based framework. This means that every piece of HTML we write in Rust exists as a Yew component.

For more information see the official Yew website.

Using Trunk for bundling and serving WASM

To make things easier we will use Trunk for bundling and testing our code. Trunk can act as a local webserver to serve our HTML and WASM Web application. It is as easy as placing a index.html, with appropriate content, in our project’s root directory and running the command: trunk serve.

Installation instructions for Trunk can be found on Trunk’s home page.

Project setup

Let’s create our first WebAssembly Rust front-end project with Yew: cargo new yew-basic-web-app.

Next, we’ll add the crates we will use to the dependencies section in the Cargo.toml file. We’ll be using the following crates:

  • yew: A framework for making client-side single-page apps.
  • yew-router: A router implementation for the Yew framework.
  • web-sys: Bindings for all Web APIs, a procedurally generated crate from WebIDL.
  • log: A lightweight logging facade for Rust.
  • wasm-logger: A logger that sends a message with its Rust source’s line and filename to the browser console.
[package]
name = "yew-basic-web-app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = "0.19"
yew-router = "0.16"
web-sys = "0.3.57"
log = "0.4.6"
wasm-logger = "0.2.0"

Minimal WebAssembly Rust Yew front-end App

In this section, we are going to create a minimal Web App. Our initial Web App will have only an index.html and main.rs. Our Yew code will be injected into this HTML file. For now, we will see some very simple code to demonstrate that what write with Rust shows up on the HTML page.

Adding the index.html

First, we have to add an index.html file to the root directory of our project, for example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Yew basic Single Page App</title>
</head>
<body>
    
</body>
</html>

That is all we need for now. Our App’s HTML and code will be in our Rust source files. However, in later sections, we will add some CSS. So, we will also look at an example of how to reference CSS in this file and have it bundled by Trunk.

Adding Yew code to main.rs

Next, we will write some Rust code for the front-end with items from the Yew crate which will be turned into WebAssembly.

What we need at a minimum is:

  • Import everything from yew with use yew::prelude::*
  • Create a (function) component that represents the HTML content
  • Start the app using yew:start_app and passing in the component as the type

First, let’s look at the code and then go through and explain it:

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
        <div class="container">
            <h1>{"Basic Yew Web App"}</h1>
        </div>
        </>
    }
}

fn main() {
    yew::start_app::<App>();
}

App function and components

The main part we want to look at here is the app() function. This function represents the main app’s root HTML. Our function has an attribute #[function_component(App)]. This is very important. Since Yew is a component-based framework. Components are the building blocks of Yew. All HTML lives inside a component.

In this case, the attribute will create a component called App based on our app function.

For more information on function components see the docs.

Our function component here is a simplified version of a normal Component. Because normally a component is defined as a struct and has to implement the Component trait with certain functions. However, using function_component allows us to write a simple component with minimal code.

Writing actual HTML

Our app function should return Html. This Html can be constructed with the html! macro. The html! macro allows us to write HTML and SVG code in a similar fashion as JSX.

More detailed information here.

Because we can only have one root element in this html! macro we open with an empty tag <> on line 5, which is allowed. The rest is normal-looking HTML, except for how the text in the <h1> tag is declared. Literals must always be quoted and wrapped in braces. That’s why we wrote <h1>{"Basic Yew Web App"}</h1> there.

Initializing and running the app

In the main() function we initialize the app with yew::start_app::<App>();.

Now we can start the app using the Trunk command: trunk serve

The output in the terminal should look something like this:

Trunk running console output. Server listening at 0.0.0.0:8080

As we can see a server is started locally for hosting our app. This server is listening on port 8080. So if we open a browser and navigate to localhost:8080 we should see our app:

Screenshot of web browser showing our basic webassembly rust front-end yew app.

Great! Next, let’s add some interactive elements.

Logging to the console

Before we proceed with interactive elements. Let’s first take a moment to enable logging to the web browser’s developer console. This will help us when debugging our Rust and Yew WebAssembly front-end programs in the future. Because we can log messages to this console that shows what is going on in our code.

To enable this type of logging we simply have to initialize the wasm_logger from the wasm_logger crate in main.rs:

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
        <div class="container">
            <h1>{"Basic Yew Web App"}</h1>
        </div>
        </>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    log::debug!("App is starting");
    yew::start_app::<App>();
}

Here we added 2 lines: 15 and 16. Line 15 is how the logger is initialized. This will then rout messages created with log to the web console.

Now when the app starts we should see a message in the console (open the Developer tools):

Logging to web console example output.

Components and properties with Yew

In this section, we are going to learn more about components. We are also going to learn about properties. Properties are a way for parent components to influence child components. This is very common in JS frontend frameworks as well.

We will create a new component using the struct and impl way of defining a component. With this component, we will show a number input element and we will cap the maximum value using Rust code. In doing this, we will learn about input and handling events.

Implementing a Yew component struct

Let’s add a new file for our component: capped_input_component.rs.

At first, we’ll make it a basic component without properties. The component can be an empty struct that implements the Component trait. For the Component trait we are then required to implement two associated types:

  • type Message: is used to send messages to a component after an event has taken place. For example after a user clicks a button. This type is often an enum.
  • type Properties: represents the information passed to the component from its parent. This type must implement the Properties trait.

We also have to implement functions: create and view:

use yew::{Component, html, Html};

pub struct CappedInputComponent {}

impl Component for CappedInputComponent {
    type Message = ();
    type Properties = ();
    fn create(_ctx: &yew::Context<Self>) -> Self {
        Self  {}
    }

    fn view(&self, _ctx: &yew::Context<Self>) -> Html {
        html! {
            <div>
                {"Capped input component"}
            </div>
        }
    }
}

For now, we leave the associated types (Message and Properties) empty. We return an empty Self or, in other words, CappedInputComponent instance on line 9 from the create function.

Then on line 12 we define the view function which returns a single div with some text. Just to see the custom component working.

Note that we write the ctx parameter as _ctx this is to suppress compiler warnings due to unused parameter variables.

Using the custom component

Let’s use our component in the main HTML in main.rs:

use yew::prelude::*;
use capped_input_component::CappedInputComponent;

mod capped_input_component;

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
        <div class="container">
            <h1>{"Basic Yew Web App"}</h1>
            <div>
                <CappedInputComponent/>                
            </div>
        </div>
        </>
    }
}

fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    log::debug!("App is starting");
    yew::start_app::<App>();
}

Here we pull CappedInputComponent into scope from our module on line 2. And then we use the component in our HTML on line 13.

If we had trunk serve running the web page will automatically update each time we save our changes. Our current web page looks like this:

WebAssembly Rust front-end with Yew: custom component example.

Component properties

In this section, we will create a property struct for our component. This will allow the parent component to set values on our capped input component. We will use the properties value to limit the value of a number input.

Let’s update the capped_input_component.rs file:

use yew::{Component, html, Html, Properties};

#[derive(Properties, PartialEq)]
pub struct CappedInputProps {
    pub min_value: u32,
    pub max_value: u32
}

First, at the top, we add a new struct definition. Here we define the properties we want our component to have. In this case, we have decided to add min and max value properties as unsigned integers.

We should also make sure to derive from Properties and PartialEq. This is required by the Yew framework. The PartialEq is required so that Yew can do comparisons.

Using properties in the Component implementation

Next, we should update the impl Component for CappedInputComponent block with the new properties struct. We will also update the view code with text showing the properties as well as an input control with min and max attributes. These attributes can be set by the properties of our component:

pub struct CappedInputComponent {}

impl Component for CappedInputComponent {
    type Message = ();
    type Properties = CappedInputProps;
    fn create(_ctx: &yew::Context<Self>) -> Self {
        Self  {}
    }

    fn view(&self, ctx: &yew::Context<Self>) -> Html {

        let min_val = ctx.props().min_value;
        let max_val = ctx.props().max_value;
        html! {
            <div>
                <label>{format!("Input value. Min: {}, Max: {}", min_val, max_val)}</label>
                <input type="number" placeholder="input a number" min={min_val.to_string()} max={max_val.to_string()}/>              
            </div>
        }
    }
}

We set type Properties to our CappedInputProps struct on line 13.

Then we should make sure to rename _ctx to ctx on line 18 in the function definition. This ctx allows us to access the properties within the component. As the name implies, this is the component’s context.

To make using the values more convenient we assign the values to local variables on lines 20 an 21.

We show the values in a label tag on line 24 where we use the format! macro to produce a parameterized string.

Finally, we add the input element with type of "number" to our HTML and set the element properties min and max with the properties from our component. These values have to be converted to strings to work with the attributes of the HTML element.

Setting properties from the parent component

Now that we have the properties implemented for our CappedInputComponent we are seeing errors in main.rs since the properties are required to be set on the component:

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
        <div class="container">
            <h1>{"Basic Yew Web App"}</h1>
            <div>
                <CappedInputComponent min_value={0} max_value={20}/>  
            </div>
        </div>
        </>
    }
}

Here we use the properties defined on the component as if it is a normal HTML element on line 13. Now when we run the web app we should see the following:

WebAssembly Rust front-end with Yew: input element with properties set using Rust code.

Now that we can set properties on our component we can easily add another instance with a different set of constraints:

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
        <div class="container">
            <h1>{"Basic Yew Web App"}</h1>
            <div>
                <CappedInputComponent min_value={0} max_value={20}/>  
                <CappedInputComponent min_value={5} max_value={30}/>  
            </div>
        </div>
        </>
    }
}

The result:

WebAssembly Rust front-end with Yes: multiple instances of custom component with different values for properties.

Note that we can not set a value higher than 20 using the up and down arrows on the control or mouse scroll wheel. However, we can still type in a number that exceeds the bounds we set:

WebAssembly Rust front-end with Yew: typing input can exceed limits.

We can change this behavior using Events.

Handling input events

In this section, we are going to learn about handling events using a callback function, and how to use the update function of the component life cycle to update the component.

We are going to listen for the input event (oninput) to see changes in the input element for all types of input (typing, scroll-wheel, up and down buttons).

Implementing a callback for the input event

First, we have to write a callback which will be called every time there is an input event. Callbacks are used to communicate with services, agents, and parent components within Yew. A function passed to callback always has e parameter. In the case of the oninput event this should be InputEvent.

More information on callbacks here.

We can define the callback for the input event in the view function in capped_input_component.rs:

use web_sys::{HtmlInputElement};
use yew::{Component, html, Html, Properties, InputEvent, TargetCast};

#[derive(Properties, PartialEq)]
pub struct CappedInputProps {
    pub min_value: u32,
    pub max_value: u32
}

pub struct CappedInputComponent {}

impl Component for CappedInputComponent {
    type Message = ();
    type Properties = CappedInputProps;
    fn create(_ctx: &yew::Context<Self>) -> Self {
        Self  {}
    }

    fn view(&self, ctx: &yew::Context<Self>) -> Html {

        let min_val = ctx.props().min_value;
        let max_val = ctx.props().max_value;

        let on_input = ctx.link().callback(move |e: InputEvent| {
            let input_el: HtmlInputElement = e.target_unchecked_into();

            let val: u32 = match input_el.value().parse() {
                Ok(val) => val,
                Err(err) => {
                    log::error!("error ocurred parsing value: {}", err);
                    0
                }
            };

            log::debug!("Input value: {}", val);
          
        });
        html! {
            <div>
                <label>{format!("Input value. Min: {}, Max: {}", min_val, max_val)}</label>
                <input  type="number" placeholder="input a number" oninput={on_input} min={min_val.to_string()} max={max_val.to_string()}/>              
            </div>
        }
    }
}

Here on lines 24 to 37 we define the Callback<InputEvent> using ctx.link().callback(). We pass a closure into callback. This closure will ultimately be called upon an input event.

For now, this function simply retrieves the value from the associated element. We then log a message to the console.

To retrieve the value from the input element we have to cast the InputEvent to a HtmlInputElement using e.target_unchecked_into(). This allows us to then get the value using input_el.value(). When we call this function we get a String. However, we are interested in numbers so we try to parse the String to a u32 on lines 27-33. We assign the result to val.

Finally, we log a debug message showing the value of val. In the next section, we will manipulate this value to restrict its value.

To trigger our callback on input we have to set the oninput attribute on the input HTML element. We do this on line 41: <input type="number" placeholder="input a number" oninput={on_input} min={min_val.to_string()} max={max_val.to_string()}/>

Callback results in debug message

When the app runs and we type into one of the input fields we should see the following output in the console of the browser’s developer tools:

WebAssembly Rust front-end with Yew: debug message printed each time input event occurs.

Sending and processing internal component messages

In this section, we are going to write code to send a message with some value from our on_input callback and handle it some place else.

To accomplish this we need to add a Message enum and make use of the update part of the component life cycle.

Sending messages to update the component

To update the component we need to use the update method. This is placed in the component lifecycle (create, view, update) where elements should be changed. Then, to tell the update method what to do we can use the Message type. Since there can be multiple types of updates we want to do the Message type is recommended to be an enum.

First, let’s set up the enum for changing the value on the input element:

use web_sys::{HtmlInputElement};
use yew::{Component, html, Html, Properties, InputEvent, TargetCast};

pub enum Msg {
    SetInput(u32)
}

We want to be able to pass a value, because want to set to the update function so our enum value is in the form of SetInput(u32) to facilitate that.

Now we can set the type Message to Msg on line 17:

impl Component for CappedInputComponent {
    type Message = Msg;
    type Properties = CappedInputProps;
    
    fn create(_ctx: &yew::Context<Self>) -> Self {
        Self  {}
    }
...
}

Now, we should update our callback to return the Msg enum:

        let on_input = ctx.link().callback(move |e: InputEvent| {
            let input_el: HtmlInputElement = e.target_unchecked_into();

            let val: u32 = match input_el.value().parse() {
                Ok(val) => val,
                Err(err) => {
                    log::error!("error ocurred parsing value: {}", err);
                    0
                }
            };

            log::debug!("Input value: {}", val);
            Msg::SetInput(val)
        });

Handling the messages in the update function

Now that we are returning a message from our callback we need some place to handle the message and take action. We can do that in the the update function:

    fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::SetInput(val) => {
                log::debug!("Setting input value to: {}", val);
                true
            }
        }
    }

Here we have a simple implementation of the update function. We only have one arm of the match and all it does is show the value of val passed to update in the msg. The update function returns a boolean value indicating whether or not the component should be rendered.

Running the app we will see this value get logged as a debug message in the update function:

WebAssembly Rust front-end with Yew: show debug from update function.

Updating the input HTML element

In this next part of the WebAssembly Rust front-end with Yew project, we are going to check the value coming in through the oninput callback and restrict it to the configured maximum and minimum values. Then we will set the value of the input element in the update function.

Change input value to adhere to restrictions

We’re going to implement a check in our callback and limit the input value first. We compare the incoming value to the max and min allowed value and then change the value if necessary:

       let on_input = ctx.link().callback(move |e: InputEvent| {
            let input_el: HtmlInputElement = e.target_unchecked_into();

            let mut val: u32 = match input_el.value().parse() {
                Ok(val) => val,
                Err(err) => {
                    log::error!("error ocurred parsing value: {}", err);
                    0
                }
            };

            log::debug!("Input value: {}", val);
            if val > max_val {
                val = max_val;
            } else if val < min_val {
                val = min_val
            }

            Msg::SetInput(val)
        });

However, doing this will not change the value seen in the web app in the input element. Because we need to change the value on the HTML element itself as well.

Getting an HTML element using Yew’s ref and NodeRef

Before we can set the value on an HTML element we first need to know to reference the element somehow. For this, we can use the ref keyword and a NodeRef object. This ref keyword is used to be able to retrieve a component or DOM HTML Element. See the official document on NodeRef for more detailed information.

Let’s first add a field to our component struct to hold the NodeRef for the input element, and also initialize it in the create function:

use web_sys::{HtmlInputElement};
use yew::{Component, html, Html, Properties, InputEvent, TargetCast, NodeRef};

pub enum Msg {
    SetInput(u32)
}

#[derive(Properties, PartialEq)]
pub struct CappedInputProps {
    pub min_value: u32,
    pub max_value: u32
}

pub struct CappedInputComponent {
    input_ref: NodeRef
}

impl Component for CappedInputComponent {
    type Message = Msg;
    type Properties = CappedInputProps;
    
    fn create(_ctx: &yew::Context<Self>) -> Self {
        Self  { input_ref:NodeRef::default() }
    }
...
}

Then we can use this on for the input element attribute ref as a value:

    fn view(&self, ctx: &yew::Context<Self>) -> Html {

        let min_val = ctx.props().min_value;
        let max_val = ctx.props().max_value;

        let on_input = ctx.link().callback(move |e: InputEvent| {
            let input_el: HtmlInputElement = e.target_unchecked_into();

            let mut val: u32 = match input_el.value().parse() {
                Ok(val) => val,
                Err(err) => {
                    log::error!("error ocurred parsing value: {}", err);
                    0
                }
            };

            log::debug!("Input value: {}", val);
            if val > max_val {
                val = max_val;
            } else if val < min_val {
                val = min_val
            }

            Msg::SetInput(val)
        });
        let input_ref = self.input_ref.clone();
        html! {
            <div>
                <label>{format!("Input value. Min: {}, Max: {}", min_val, max_val)}</label>
                <input ref={input_ref} type="number" placeholder="input a number" oninput={on_input} min={min_val.to_string()} max={max_val.to_string()}/>              
            </div>
        }
    }

Set the input value on the HTML element with Yew

Now that we have a reference to the HTML element we can manipulate this element in the update function with our Rust code.

Let’s add the following code to the update function:

    fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::SetInput(val) => {
                log::debug!("Setting input value to: {}", val);
                let input_element = self.input_ref.clone().cast::<HtmlInputElement>().unwrap();
                input_element.set_value(&val.to_string());
                true
            }
        }
    }

Since we know the element being referred to is an input element we can cast the input_ref to a HtmlInputElement object without problems.

Then we can call the set_value function to set the value on the HTML element. We have to convert the value to a string reference.

Completed input value restriction

So, now we can run the application again and see that when we type in a value that exceeds the restrictions we configured it will no longer be displayed. Instead, our code changes the value to the max or the minimum value depending on what was typed.

WebAssembly Rust front-end with yew:
Debug output restricting value on input

Opening the developer tools and looking at the console we should see a message “Input value: 200” and “Setting input value to: 20” when typing in 200.

Updating an internal list

In this section, add managing a list to our WebAssembly Rust Front-end with Yew project. We will look at another example of how Yew handles the internal state of a component. What this means is, that we can set a variable and Yew will internally keep track of its state so its value is remembered. If the variable is referenced in the HTML we can see it change in the UI when it gets updated.

We are going to do something that is quite similar to updating the input element. However, this time we are going to update a list (Vec) instead.

Let’s add a button that when clicked will add our number input to a list.

Preparing our struct: CappedInputComponent

The state of the list can easily be tracked using a field on the CappedInputComponent struct. If we want to change this field, though, we have to do it through the message system and the update function. This means we have to add a new message for the “add input to list” action:

use web_sys::{HtmlInputElement};
use yew::{Component, html, Html, 
    Properties, InputEvent, TargetCast, NodeRef, MouseEvent};

pub enum Msg {
    SetInput(u32),
    AddValue(u32),
    DoNothing
}

#[derive(Properties, PartialEq)]
pub struct CappedInputProps {
    pub min_value: u32,
    pub max_value: u32
}

pub struct CappedInputComponent {
    input_ref: NodeRef,
    value_list: Vec<u32>
}

We add a use of MouseEvent on line 3 since we want to handle a mouse click on the button event. Then on lines 7 and 8 we have some new message types. First, AddValue is for when we want to add a value to our list. Second, DoNothing is for cases where the button is clicked but the input field is empty for example. Since we don’t want to add empty values.

Since our struct has a new field we also have to update the create function to initialize the default value. In this case an empty list:

impl Component for CappedInputComponent {
    type Message = Msg;
    type Properties = CappedInputProps;

    fn create(_ctx: &yew::Context<Self>) -> Self {
        Self  { input_ref:NodeRef::default(), value_list: Vec::new() }
    }

Handle new messages in update

Next, we have to add new arms to our match expression in the update function. Here we can add the new value to our list or tell the update to do nothing:

    fn update(&mut self, ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::SetInput(val) => {
                log::debug!("Setting input value to: {}", val);
                let input_element = self.input_ref.clone().cast::<HtmlInputElement>().unwrap();
                input_element.set_value(&val.to_string());
                true
            },
            Msg::AddValue(val) => {
                log::debug!("Adding value to list: {}", val);
                self.value_list.push(val);
                true
            },
            Msg::DoNothing => {
                log::debug!("Do nothing");
                true
            }
        }
    }

We do some extra logging so we can easily follow what is going on through the browser’s console outputs. Then in the Msg::AddValue(val) arm, we simply push the new value onto the value_list list field.

On click callback to send an update message

Now we can add a new callback function and a button. Our callback function will take the value from the input element, parse it and then return the appropriate message (AddValue or DoNothing). Here is the full code listing of the update view function. Let’s go through the changes piece by piece after looking at the whole thing:

    fn view(&self, ctx: &yew::Context<Self>) -> Html {

        let min_val = ctx.props().min_value;
        let max_val = ctx.props().max_value;

        let on_click = {            
            let cur_input = self.input_ref.clone();
            ctx.link().callback(move |_e: MouseEvent| {
                let value_input_element = cur_input.cast::<HtmlInputElement>().unwrap(); 
                let new_value = value_input_element.value();
                value_input_element.set_value("");
                match new_value.parse() {
                    Ok(val) => Msg::AddValue(val),
                    Err(_) => {
                        log::debug!("Error occured parsing '{}'", new_value);
                        Msg::DoNothing
                    }
                }
            })
        };

        let on_input = ctx.link().callback(move |e: InputEvent| {
            let input_el: HtmlInputElement = e.target_unchecked_into();

            let mut val: u32 = match input_el.value().parse() {
                Ok(val) => val,
                Err(err) => {
                    log::error!("error ocurred parsing value: {}", err);
                    0
                }
            };

            log::debug!("Input value: {}", val);
            if val > max_val {
                val = max_val;
            } else if val < min_val {
                val = min_val
            }

            Msg::SetInput(val)
        });

        let display_values = self.value_list.iter().map(|value| html!{<li>{value}</li>});
        html! {
            <div>
                <div>
                    <label>{format!("Input value. Min: {}, Max: {}", min_val, max_val)}</label>
                    <input ref={self.input_ref.clone()} type="number" placeholder="input a number" oninput={on_input} min={min_val.to_string()} max={max_val.to_string()}/>              
                    <button onclick={on_click}>{"Add to list"}</button>
                </div>
                <div>
                    <ul>
                    {for display_values}
                
                    </ul>
                </div>
            </div>
        }
    }

On input callback explanation

Now let’s look at each part. Starting with the on_click callback function:

        let on_click = {            
            let cur_input = self.input_ref.clone();
            ctx.link().callback(move |_e: MouseEvent| {
                let value_input_element = cur_input.cast::<HtmlInputElement>().unwrap(); 
                let new_value = value_input_element.value();
                value_input_element.set_value("");
                match new_value.parse() {
                    Ok(val) => Msg::AddValue(val),
                    Err(_) => {
                        log::debug!("Error occured parsing '{}'", new_value);
                        Msg::DoNothing
                    }
                }
            })
        };

On line 56 here we clone the input_ref value to be able to move it into the closure for the callback. This then allows us to cast the reference to a HtmlInputElement instance to get the value on lines 58 and 59 respectively.

Once we get the value we want to clear the input to make it clear the value was submitted and to make it easier to enter a new value for the user. So, we simply call .set_value("") on the element with an empty string like that.

Finally, we attempt to parse the value to an u32 value type to make sure the value is valid. If the value is Ok(val) then we return a Msg::AddValue message to tell our update function to add the value. Otherwise, if the parse fails, we send a Msg::DoNothing message.

The button element and list view

Let’s look at the last part now, the part where the list is generated and shown as well as the button element.

      let display_values = self.value_list.iter().map(|value| html!{<li>{value}</li>});
        html! {
            <div>
                <div>
                    <label>{format!("Input value. Min: {}, Max: {}", min_val, max_val)}</label>
                    <input ref={self.input_ref.clone()} type="number" placeholder="input a number" oninput={on_input} min={min_val.to_string()} max={max_val.to_string()}/>              
                    <button onclick={on_click}>{"Add to list"}</button>
                </div>
                <div>
                    <ul>
                    {for display_values}
                
                    </ul>
                </div>
            </div>
        }

We want to display the list of values as an HTML unordered list (<ul></ul>) so each item is a HTML list item (<li></li>).

Therefore we are turning our Vec of values into an iterator on HTML elements using iter() and map() functionality on line 92. This iterator can then be used in a for loop in our main HTML on line 102. This {for display_values} will insert each <li></li> element into our HTML.

Aside from that we also add a button to our HTML definition on line 98. We set the onclick attribute value to our callback on_click. As the name suggests this is the event hook for when the button is clicked.

Inspecting the result

Now let’s look at the result. We should see a button and a growing list when entering values and clicking the button:

WebAssembly Rust front-end with yew: add to list button and growing list.

Managing state for a function component with use_state

We have learned how to manage data internally in a struct component, in other words, a struct that implements Component. However, what if we want to create a simpler component using function_component and still, manage internal state? In this last section of part1 of our WebAssembly Rust front-end with Yew project, we will look at how use_state can be used.

Implementing the list component as a function_component

Let’s create a new file in the src directory called list_component.rs. This will hold the code for our new component defined using function_component.

The code has a lot of overlap with the code in the view function for our component called CappedInputComponent. However, there are some key differences.

First, we will look at the whole code listing and then go through the parts one by one:

use web_sys::{HtmlInputElement};
use yew::{prelude::*};

#[function_component(ListComponent)]
pub fn list_component() -> Html {
    let name_list = use_state(Vec::new);
    let input_ref: NodeRef = NodeRef::default();

    let on_click = {
        let name_list = name_list.clone();
        let cur_input = input_ref.clone();
        Callback::from(move |_e: MouseEvent| {
            let mut names = (*name_list).clone();
            let name_input_element = cur_input.cast::<HtmlInputElement>().unwrap(); 
            let new_name = name_input_element.value();
            name_input_element.set_value("");
            names.push(new_name);
            name_list.set(names);
            
        })
    };

    let display_names = (*name_list).iter().map(|name| html!{<li>{name}</li>});
    html!{
        <div>
            <div>
            {"List component"}
            </div>
            <div>
                <input ref={input_ref} type="text" placeholder="input a name"/>
                <button onclick={on_click}>{"Add to list"}</button>
            </div>
            <div>
                <ul>
                {for display_names}              
                </ul>
            </div>
        </div>
    }
}

Not much difference in the html! part, so let’s talk about use_state.

What is use_state

According to the documentation use_state returns a UseStateHandle object which we can deref to the current value and it gives us a method to update the value of our variable. To deref something in Rust we use *variablename.

In the first part of the function component we set up a variable with use_state and also a NodeRef. We will use NodeRef to refer to the HTML input element.

use web_sys::{HtmlInputElement};
use yew::{prelude::*};

#[function_component(ListComponent)]
pub fn list_component() -> Html {
    let name_list = use_state(Vec::new);
    let input_ref: NodeRef = NodeRef::default();

Since we want to track the state of a list we pass Vec::new as a parameter to use_state. We don’t have to specify a type to be stored in the Vec here, because the compiler will figure it out from context later.

Implementing the on_click callback

Now let’s look at the callback implementation:

    let on_click = {
        let name_list = name_list.clone();
        let cur_input = input_ref.clone();
        Callback::from(move |_e: MouseEvent| {
            let mut names = (*name_list).clone();
            let name_input_element = cur_input.cast::<HtmlInputElement>().unwrap(); 
            let new_name = name_input_element.value();
            name_input_element.set_value("");
            names.push(new_name);
            name_list.set(names);
            
        })
    };

Before we can use our name_list in the callback we have to move it into the closure. However, since we want to use name_list to get the values out later in this list_component function, we have to clone it in a new scope on line 10. The same is true for input_ref.

Next, on line 13 we deref the name_list and also clone it to move it out of the derefence so we can have a mutable object.

Then on line 14, we cast the cur_input which is a ref to the input element to a HtmlInputElement object. So we can get the value from it on line 15 and then clear the value in the element on line 16.

Finally, we push the new value new_name into the list on line 17 and then use the set function from the UseStateHandle object to update the state of our Vec.

Displaying our list in HTML

The last thing we have added is the HTML displaying the input, button, and list:

  let display_names = (*name_list).iter().map(|name| html!{<li>{name}</li>});
    html!{
        <div>
            <div>
            {"List component"}
            </div>
            <div>
                <input ref={input_ref} type="text" placeholder="input a name"/>
                <button onclick={on_click}>{"Add to list"}</button>
            </div>
            <div>
                <ul>
                {for display_names}              
                </ul>
            </div>
        </div>
    }

We have to dereference the name list (name_list) again so we can iterate over the Vec since name_list itself is a UseStateHandle object. Then we use map to construct an iterable containing <li>{name}</li> as we did for our other component as well.

The rest of the HTML is pretty much the same as what we wrote earlier for our CappedInputComponent component.

The function_component Result

Before we can look at the result we have to add our new component to the main HTML in main.rs:

use yew::prelude::*;
use capped_input_component::CappedInputComponent;
use list_component::ListComponent;

mod capped_input_component;
mod list_component;

#[function_component(App)]
fn app() -> Html {
    html! {
        <>
        <div class="container">
            <h1>{"Basic Yew Web App"}</h1>
            <div>
                <CappedInputComponent min_value={0} max_value={20}/>  
                <CappedInputComponent min_value={5} max_value={30}/>  
                <ListComponent/>
            </div>
        </div>
        </>
    }
}

Let’s run or refresh the application again (if it didn’t refresh automatically) and look at the result:

WebAssembly Rust front-end with Yew: showing function_component component.

Conclusion

Through this simple WebAssembly Rust Front-end with Yew project, we have learned about components, events, internal state, and elements. We looked at how to write a component by using impl Component and also using function_component for simpler more compact code. Furthermore, we looked at how events work in Yew and how messages can be used to update our component.

The complete code can be found on my github here: https://github.com/tmsdev82/yew-basic-web-app

Part two of the tutorial is here: CSS and JavaScript WASM Rust: Yew how to P2.

Please follow me on Twitter to get notified of new Rust programming articles:

Comments (2)
  • Wow, thank you so much for this. I was trying so hard to figure out how to use yew with rust and I found dead-end after dead-end, this tutorial has walked me through a lot of things I was looking for on the web. 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *