Somehow the last article about Yew got more traction than I anticipated. Not much, but still!
That's why I sat down and played a bit around with Yew again, in the hope that one day a Yew maintainer will jump in and call me out on the Rust/Yew crap I'm teaching people here :D
I still hope to get a bit better at Rust, but I have to admit this framework made most of the things pretty easy. I didn't fight the borrow checker much until now. But I'm still in the "let's sprinkle some &s here and there"-stage without really understanding what's going on, haha.
In this article, we will build components with fundamental interactions. Again, nothing fancy, just a counter and a text-area. They don't even interact with each other.
TL;DR: The finished app can be found on GitHub.
Setup
I won't go into installing Rust, wasm_bindgen, or setting up a Yew project. All that can be found in the previous article.
Creating the Main Component
Like in the previous article, we are going to have three components. The first one is the actual app and lives in src/lib.rs
. It has the following code:
#![recursion_limit="1000"]
mod counter;
use counter::Counter;
mod text;
use text::Text;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
struct Model {}
impl Component for Model {
type Message = ();
type Properties = ();
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self {}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<Counter />
<Counter />
<Text />
<Text />
</div>
}
}
}
#[wasm_bindgen(start)]
pub fn run_app() {
App::<Model>::new().mount_to_body();
}
It doesn't do anything but rendering the other two components we will implement. Compared to the previous article, nothing new here.
Creating the Counter Component
The counter component displays a number that can be incremented and decremented; the 101 of UI interactions. Create a new file at src/counter.rs
and add this code:
use yew::prelude::*;
pub struct Counter {
link: ComponentLink<Self>,
value: i64,
}
pub enum Msg {
Increment,
Decrement,
}
impl Component for Counter {
type Message = Msg;
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
value: 0,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Increment => self.value += 1,
Msg::Decrement => self.value -= 1,
}
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<button onclick=self.link.callback(|_| Msg::Increment)>{"+"}</button>
<span style="width: 50px">{ self.value }</span>
<button onclick=self.link.callback(|_| Msg::Decrement)>{"-"}</button>
</div>
}
}
}
First, there is a struct
that holds our state. It consists of a link
and a value
. The link
is required to execute our component methods later, for example, when a button is clicked. The value
is the number that can be updated later.
Second, there is an enum
that defines messages. Messages can be used with the update
method of our component to tell it what to do. In this case, we have two messages, Increment
and Decrement
.
We have to set the Message
type of the Component trait to our Msg
enum
, so the type inferences work correctly (I guess?).
The create
method sets the value
to zero when a new component instance is created.
The update
method is responsible for updating the state. It receives Increment
or Decrement
messages and acts accordingly on our value
state.
The change
method doesn't do anything because our component doesn't accept any properties from its parent.
The view
gets called when the update
method returned a true
, which it always does. If we had some background state that isn't visible to the user, we could skip a render by returning a false
from the update
method.
In the view
method, we can also see the use of the link
state. The callback
method of the link
receives ... well ... a callback/closure. This callback has to return the message (Increment
or Decrement
) that we want to handle in the update
method from above.
Overall, the whole thing isn't much different from React or Vue, just a bit more code required.
Creating the Text Component
The text component is a single textarea
element that listens to its inputs and renders out upper-case versions of the text entered into it.
Create a src/text.rs
file with this code:
use yew::prelude::*;
pub struct Text {
link: ComponentLink<Self>,
content: String,
}
pub enum Msg {
Update(String),
}
impl Component for Text {
type Message = Msg;
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { link, content: "".to_string() }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Update(content) => self.content = content.to_uppercase(),
}
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<textarea
oninput=self.link.callback(|event: InputData| Msg::Update(event.value))
value=&self.content>
</textarea>
}
}
}
This component is mostly the same as the last. The main difference is that it only one message, but this time the message has some data.
In this case, a String
value. To get the data out of the textarea
element and into our Msg::Update
message, we have to look into the view
method.
The oninput
callback uses the event
argument this time. It's of type InputData
and comes with its fresh value
. We can call Msg::Update(event.value)
to wrap our new value with our message, and both will be on their way to the update
method.
The update
method uses match
again to check which message we received. Still, this time it destructures the message so we can use the content
(previously value
) to update the state of our component; we also call to_uppercase
on it, a method of String
, to make it evident that the new text went through this method. Since we return true
, the view
method will be called by the framework after updating and rendering our new text.
Summary
You get an update/view loop going that is triggered by you clicking/typing things, same as with JavaScript/React. Since the components are encapsulated, I can do my thing in one component without looking into another.
Sure, Rust is much wordier than JavaScript, but then you get more helpful error messages. Also, enums and pattern matching is a very powerful tool that is missing in JavaScript and I could imagine in the long run worth the extra code to write.
I found this to be a good Rust exercise. I learned that I still doesn't understand the module system, let alone macros.