YEW Tutorial: 02 Extending the example
Davide Del Papa
Posted on April 12, 2020
Cover photo by Daria Nepriakhina on Unsplash; the banana, after Cattelan's exposition, is coming to signify the message of deceptive simplicity.
In this second part we are going to do some extensions/improvements,
and hopefully show a little more of the potential
of Yew framework
In this article we will be tinkering around, in order to understand better the concepts
at work in Yew. We will do also some project related stuff, which is more general or more front-end specific, and less related with Yew. Let's go!
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/v2p0
Let's script our way through the boring stuff
But first a little improvement that will help us a lot.
Let's create a file called run.sh
run.sh
This script's' purpose is simple: automate the build and run of this project.
#!/bin/sh
build(){
wasm-pack build --target web
}
pack(){
rollup ./main.js --format iife --file ./pkg/bundle.js
}
run(){
python -m "http.server" "8080" &
PID=$!
echo $PID > .serverpid
}
build
pack
run
At the top of the script we have the shebang, #!/bin/sh
,that tell the console interpreter which environment use.
Of course, sh is not bash, but as long as you are posix compliant in your scripts, it doesn't matter. I personally use zsh with the fabulous Oh My Zsh!, but these are personal preferences. Back to work, now.
We divided the actions to be done in 3 functions that we call at the end.
It is important to be modular, because in this way we can modify independently each one of the functions.
- the function build calls up the
wasm-pack
as we have seen in the previous post - the function pack calls up the
rollup
as seen in the previous post.
The run function deserves special attention. We are not simply calling the server, we are making it work in background (the &
appended to the command). For this reason we are saving the process' PID and storing it in a hidden file, .serverpid
Quick troubleshooting: if you have both python2 and python3 installed on your system, and the script is not working, saying it can't recognize the http module
/usr/bin/python: No module named http
try calling straight python 3, for example python -m "http.server" "8080" &
Now that it is done, let's make it executable.
chmod u+x run.sh
Finally we can execute it easily with:
./run.sh
Yatta! All working well. But if we wish to stop the server? We need to look at the process' pid we stored earlier
cat .serverpid
Right now for me is 10859
I can stop it, therefore with:
kill 10859
Done!
Now we'd better make an automated script to do just that-
stop.sh
The script is modular as well, maybe its overkill, but better be prepared for more.
#!/bin/sh
stop(){
if [ -f .serverpid ]; then
kill $(cat .serverpid)
rm .serverpid
echo Server stopped
else
echo Could not find the server PID. Is it running?
fi
}
stop
- We check for the existence of .serverpid and we kill the process with the pid written in it
- We alert the user both in the case the pid was not found and in the case all went well.
Now we have two scripts to help us going on with the project (remember to make stop.sh executable!)
Extending our example app
After this detour we can go on with the Yew tutorial.
Without further ado, let's thinker around with the code inside our src/app.rs
Part 1: Add a new message
Code to follow this part
git checkout tags/v2p1
First of all, let's add a new message, let's say, to decrease the counter.
pub enum Msg {
AddOne,
RemoveOne,
}
Now we need to take care of handling the logic of this new message:
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddOne => self.counter += 1,
Msg::RemoveOne => self.counter -= 1,
}
true
}
Quite simply, we added a match branch and we did something with that message.
It is now time to add a button to the DOM, in order to send this new message.
fn view(&self) -> Html {
html! {
<div>
<p> {"Counter: "} { self.counter }</p>
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "Add 1" }</button>
<button onclick=self.link.callback(|_| Msg::RemoveOne)>{ "Remove 1" }</button>
</div>
}
}
This too is quite self-explanatory. Let's check it out starting the server.
Well good. We can even go negative if we want. What about implementing the logic to avoid negative numbers, but stop to 0
? Can you implement it? (My solution in code)
Part 2: What if we want to handle a list instead
Code to follow this part
git checkout tags/v2p2
What if we want the counter instead to be a list, and increase the elements of that list?
This is plain ol'Rust, let's do it!
First of all, let's modify counter to be a Vec<i64>
. Actually, the name "counter does not make sense anymore, so let's rename the symbol too:
pub struct App {
items: Vec<i64>,
link: ComponentLink<Self>,
}
We need to update our component constructor as well, the create function:
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
App { link, items: Vec::new() }
}
Of course, we can't simply add to the a Vec
, we need to update the update logic!
Well... What are we going to push inside of it? For now just a random number!
Wait! How do we add a random number?
We need first of all get into our dependencies the crate rand, which supports compiling to the WASM through getrandom
In Cargo.toml add to the [dependencies]
:
rand = { version = "0.7", features = ["wasm-bindgen"] }
To call the random()
function, we have to use
the preamble in src/app.rs
use rand::prelude::*;
Then we can change the handling logic in update
:
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddOne => self.items.push(random()),
Msg::RemoveOne => {
self.items.pop();
}
}
true
}
Why would we need to wrap the remove branch of the match
in brackets? Remember that pop()
returns an Option (the popped element or None
), while push returns ()
.
Finally we have to display our trusty vec:
fn view(&self) -> Html {
html! {
<div>
<p> {"Items: "} { format!("{:?}", self.items) }</p>
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "Add 1" }</button>
<button onclick=self.link.callback(|_| Msg::RemoveOne)>{ "Remove 1" }</button>
</div>
}
}
A vector does not implement Display, so we take advantage of the debug formatting.
In fact, all handy tools are still available to us:
Rust's still Rust even compiled to WASM.
We can now run and check the results.
Well, I added some random items.
OK, but should we not do something useful instead of just Rusting around?
Hold your horses cowboy, we first need some more understanding of how to work in Rust with WASM, and the Yew framework.
Part 3: Display the Vec the nice way
Code to follow this part
git checkout tags/v2p3
Rust's format!
is great and all, but sure displaying a vec in debug mode is not that great frontend practice, is it?
Let's remedy to that, by changing our view
fn view(&self) -> Html {
let render_item = |item| {
html! {
<>
<tr><td>{ item }</td></tr>
</>
}
};
html! {
<div class="main">
<div class="card">
<header>
{"Items: "}
</header>
<div class="card-body">
<table class="primary">
{ for self.items.iter().map(render_item) }
</table>
</div>
<footer>
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "Add 1" }</button> {" "}
<button onclick=self.link.callback(|_| Msg::RemoveOne)>{ "Remove 1" }</button>
</footer>
</div>
</div>
}
}
Whaaat? Stop the world it's spinning too fast!
We are using a CSS framework here, picnic, and we are using a recursion over the vector in order to create rows of a table.
Lets go in order.
We added a lot of HTML tags and CSS classes, but really what has changed is just the following:
{ for self.items.iter().map(render_item) }
This notation starting with a for iterates over the vec self.items
and maps the results to a closure, render_item.
Let's see what this closure does, then.
let render_item = |item| {
html! {
<>
<tr><td>{ item }</td></tr>
</>
}
};
This closure returns a html!
using fragments.
Fragments are a couple of empty tags <>
and </>
. This is the way Yew has to nest code for html!
inside another. Since this macro can have just one starting point in the DOM, fragments manage the nesting for us.
For the rest, there is really no other new thing, except for a lot of new HTML and CSS classes. And a little caveat:
{" "}
This fragment between the two buttons is really important, in order to let Yew play nice with picnic buttons.
Before running the project we have to remember to update the index.html as well, in order to call picnic
, otherwise there is no point in all those CSS classes.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Yew Tutorial</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://unpkg.com/picnic" />
<script src="/pkg/bundle.js" defer></script>
<style>
.main {
max-width: 800px;
margin: auto;
}
.card-body {
margin: auto;
}
</style>
</head>
<body></body>
</html>
I added 2 styles as well, just to show that you can still integrate Yew with all the usual styling methods.
Finally, we have also to increase the recursion limit if we want our html!
macro to compile.
In fact, in order to put more elements in the html!
macro the default limit is too tight.
In src/lib.rs
#![recursion_limit = "256"]
mod app;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
yew::start_app::<app::App>();
Ok(())
}
We can run and see the rewards of our efforts!
We will continue in the next installment of this tutorial talking about services.
Posted on April 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.