Yes, I'm humbling joining the fight to widespread Rust, with a tutorial on Yew.
Code to follow this tutorial
The code has been tagged with the relative tutorial and part.
git clone https://github.com/davidedelpapa/yew-tutorial.git
cd yew-tutorial
git checkout tags/v1
Part 1: Prerequisites, AKA, installation first
This tutorial pre-se does require at least some Rust knowledge.
However, I hope it would be profitable also for the beginners. I myself am still learning the language, yet in order to enjoy the full potential of Rust as applied to WASM
technology one does not need to know all the intricacies of the type system.
A sufficient Rust knowledge is gained after few weeks of acquaintance with this language, thus I think that once the basics are learned, this guide can be very profitable also to the novices.
Of course, Rust itself must be installed. Next, we need to install wasm-pack. NOTE: I made this tutorial in a highly opinonated manner, not "just because", but just because the novices would not get lost around the many choices available.
cargo install wasm-pack
After this we'll need a JS bundler, and we'll use rollup.
npm install --global rollup
Now with thsese tools under our belt, let's move one to build soemthing.
Part 2: Let's create our first app from scratch
From our project's folder let's create a new rust project
cargo new yew-tutorial --lib
It's important that it be a lib
cd yew-tutorial
code .
I'm using code to develop, but you can use whichever IDE or plain text editor you like.
I myself many times use gedit, that simple!
Let's get over all files needed for this project, creating those missing as we go
cargo.toml
In cargo.toml let's add the following lines to the [dependencies]
section
wasm-bindgen = "^0.2"
yew = "0.12"
We need to add as well a new [lib]
section,
with the following:
[lib]
crate-type = ["cdylib"]
The file content should look like the following:
[package]
name = "yew-tutorial"
version = "0.1.0"
authors = ["you"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "^0.2"
yew = "0.12"
index.html
Let's add a barebones index.html in order to serve as the server's starting point:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Yew Tutorial</title>
<meta charset="utf-8" />
<script src="/pkg/bundle.js" defer></script>
</head>
<body></body>
</html>
The script we are refencing to, bundle.js, will be available in the pkg/ folder once the Rust lib is compiled. More on that later on.
We have left the <body>
tag empty, because we'll nest there our Yew code
Now let's move on to change the files inside the scr folder!
scr/lib.rs
For each new lib
project, cargo creates a lib.rs with a #[cfg(test)]
section for testing.
It is very useful, but for now we'll get rid of it. In its place we'll put the following:
mod app;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
yew::start_app::<app::App>();
Ok(())
}
Let's explain it step-by-step:
- we start defining the module app, which we'll create later on (file app.rs)
- we use the
wasm_bindgen::prelude::*
in order to make use of theyew::start_app
- the entrypoint for the
WASM
must be annotated, with a#[wasm_bindgen]
annotation. We will refer to it in the main.js file we will create, and available to us upon bundling. More on that later. - as the run_app() can return a JsValue wrapped in an Option, we need to specify it as the function return type, and also to return
Ok(())
Lets create our app mod next.
scr/app.rs
As we defind a mod called app, we need to create a app.rs file. Note: it is customary in the fullstack web world to call the main entry point of all web apps simply app. We as opinionated writers, will not renounce to this opinion!
Let's start our app by using the usual yew::prelude::*
use yew::prelude::*;
As customary in Rust, yew uses structs and enums to handle data related logic.
We will need one struct for handling the app, and an enum to control the messages sent to the app.
First of all, let's write down the struct App that we will implement, with the a custom state for this app.
pub struct App {
counter: i64,
link: ComponentLink<Self>,
}
As we can see the state is mantained by counter, while link is a ComponentLink which will contain the link to refer back to the current page invoking it. More on that later.
Now we'll write the enum Msg to implement the "form" logic, or controller logic, of the app.
pub enum Msg {
AddOne,
}
Let's understand the logic behind the simple example we'll use (taken from yew's docs home page).
The app we will create will present a counter and a button to increase it:
the button is like a form, in that it will link back to the page sending a message, contained in the Msg enum. Since the message is simple enough it, is sufficient we'll send the enum with the appropriate message, in this case it is present only the message to "add one to the counter", i.e., AddOne.
When the app will receive the message it will increment by one its counter, that is, the counter state of the App struct. after this, it will render the change.
It seems all complicated, but it is easier in code than explaining it.
Without further ado, we'll implement the App struct:
impl Component for App {
type Message = Msg;
type Properties = ();
We implement Yew's Component for our struct App.
In Yew each component has got a Message, used to comunicate with it, and a set of Properties.
For now we'll assign the enum Msg to be the type for Message.
Each component has three fucntions: create
, update
, and view
.
fn create
create
is the equivalent of a constructor for the component.
We'll use the return-type Self
which basically means we have to return a App struct, that is the struct we are implementing.
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
App {
link,
counter: 0
}
}
We start with the counter
set to zero.
It is good to notice that create()
will get a Properties, which we will not use, hence the undersocre, and a ComponentLink to refer to itself, which we will store in the App struct we are returning.
fn update
udate
determines the logic needed in order to handle events.
The update return type is ShouldRender which is a bool indicating if after the logic processing is done the component should render itself again or not. Returning a true or false is sufficient.
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddOne => self.counter += 1,
}
true
}
We match over the Message we receive (that is the only type it will receive), and establish that if the message is _AddOne, then it must add 1 to the internal counter.
At the end we return a true menaing that we vant to re-render the component
fn view
Finally we will "see" what our component looks like!
view
is incharged of (re-)rendering our component in the DOM.
We can return various types, in this case it is a Html object.
fn view(&self) -> Html {
html! {
<div>
<p> {"Counter: "} { self.counter }</p>
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "Add 1" }</button>
</div>
}
}
Notice first of all that the view relies only on its internal state: messages should be left where messages belong, that is, the updating logic. This is so, in order to separate the component's representation from its logic.
We'll use the html!
powerful macro in order to render components in a quasi-html style.
The rationale behind it is the same as using JSX in React: inserting elements of HTML inside a language (Rust in this case), using a kind of DSL (Domain Specific Language).
Thus, we've used <div>
, <button>
, and <p>
inside a Rust macro.
Some rules we can see:
- the value of a Rust expression must be inserted inside brackets
{}
- text must be inserted as strings, so it is an expression, thus it belongs inside brackets as well
- we have
self
to refer to the App's state.
One thing of interest: using the link to the component we activate a callback for the <button>
's onclink, to which we pass the message AddOne
Our app.rs is done!
main.js
In the root directory, let's create a main.js file, which will be the starting point for our wasm-js integration
import init, { run_app } from "./pkg/yew_tutorial.js";
async function main() {
await init("/pkg/yew_tutorial_bg.wasm");
run_app();
}
main();
We are calling here the .js and .wasm files that will be created in the directory pkg/ upon compilation. Convention wants that the names that we put in Rust, which are separated with dashes, become separated with undersocores in javascript. Thus our yew-tutorial becomes yew_tutorial.
The run_app() function will be available because it is exported from the lib.rs
Build and Run
It is now time to build and to run our first example
wasm-pack build --target web
The first build is usually longer than the following.
At the end of this we sould be greeted by a:
[INFO]: :-) Your wasm pkg is ready to publish at ./pkg.
If we check out the content we'll see the .js and .wasm files almost ready to be served.
Almost, because we need to bundle them first. We'll use the rollup tool we installed at the begnning
rollup ./main.js --format iife --file ./pkg/bundle.js
Let's serve it with our handy python http.server module.
Remember, ctrl-c
in order to shut it down:
python -m http.server 8080
Now let's head our browser to http://0.0.0.0:8080/
We can see the value starting from 0 and a button to add to that value.
Mission accomplished!
We will continue in the next installment of this tutorial talking about how to extend this simple example.
Top comments (8)
I feel
use yew::prelude::*;
should be introduced when we create app.rs to stop compile errors if someone like me is writing down code as we proceed.And thanks a ton for making this a beautiful series :) going to follow you
Yes, that in fact is now the trend for the Rust crates writers: it is now common to leave a ::prelude containing a small subset of the imports usually needed to write out programs for the common use-cases.
It is a very beginners friendly practice, but it is useful also for the more seasoned programmers.
In any case, rustc alerts you of the unused imports, so feel free to add 'use's when you are trying things out, and then clean the code once the program works.
I'm just starting out following up with Yew examples. How can hot reloading be setup ? Or what is your workflow like ?
Ok, if you take a look at the end of tutorial 6 there you have it: hot reloading working like a charm. I use two scripts still, one to start and another to stop. However I already figured out something better, trying to make it into tutorial 7 or 8... They are coming out too long I reckon, that is why I keep changing things up and writing much ahead of publication. It's lot of material that I try to cover as simply as I can, but I'm not that good sadly....
I'm currently working on the post for hot reloading. Let's say I got a little ahead writing because I have to check my English too.
Another thing hit reloading is possible but not always good, because rust takes time to compile: you have to be careful not to save too often..
Give me sometime. Maybe I will add something of a good workflow at the beginning of next post
There is another UI library named elvisjs implemented by pure rust and wasm, you might try it out if you are interested.
Thank you. I'm actually planning a comparison between various similar tools, so I will definitely try out Elvis as well!