Buttons

Our first example is a counter that can be incremented or decremented.

I included the full program below.

Put this into src/lib.rs.

1use sauron::prelude::*;
2
3#[derive(Debug)]
4pub enum Msg {
5    Increment,
6    Decrement,
7}
8
9pub struct App {
10    count: i32,
11}
12
13impl App {
14    pub fn new() -> Self {
15        App { count: 0 }
16    }
17}
18
19impl Application<Msg> for App {
20    fn view(&self) -> Node<Msg> {
21        node! {
22            <main>
23                <input type="button"
24                    value="+"
25                    key="inc"
26                    on_click=|_| {
27                        Msg::Increment
28                    }
29                />
30                <div class="count">{text(self.count)}</div>
31                <input type="button"
32                    value="-"
33                    key="dec"
34                    on_click=|_| {
35                        Msg::Decrement
36                    }
37                />
38            </main>
39        }
40    }
41
42    fn update(&mut self, msg: Msg) -> Cmd<Self, Msg> {
43        match msg {
44            Msg::Increment => self.count += 1,
45            Msg::Decrement => self.count -= 1,
46        }
47        Cmd::none()
48    }
49}
50
51#[wasm_bindgen(start)]
52pub fn main() {
53    Program::mount_to_body(App::new());
54}

Put this into your Cargo.toml.

1[package]
2name = "counter"
3version = "0.1.0"
4authors = ["Jovansonlee Cesar <ivanceras@gmail.com>"]
5edition = "2018"
6
7[lib]
8crate-type = ["cdylib"]
9
10[dependencies]
11sauron = "0.43"

Put this into you index.html.

1<html>
2  <head>
3    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
4    <title>Counter</title>
5    <style type="text/css">
6        body { font-family: verdana, arial, monospace; }
7        main {
8            width:30px;
9            height: 100px;
10            margin:auto;
11            text-align: center;
12        }
13        input, .count{
14            font-size: 40px;
15            padding: 30px;
16        }
17    </style>
18    <script type=module>
19        import init from './pkg/counter.js';
20        await init().catch(console.error);
21    </script>
22  </head>
23  <body>
24  </body>
25</html>

Compile the project using:

1wasm-pack build --release --target=web

This will produce files in pkg directory, with names derived from the crate name. In this case pkg/counter.js and pkg/counter_bg.wasm.

Main

The main function here is the entry point of our application. The function doesn't have to be named main as long as it is anotated with #[(wasm_bindgen(start)]. It tells the the runtime on where to display the application. In this case, we are going to initialize our application by calling it's fn new()->Self method. The view function is going to show everything on screen, and user input is going to be fed into the update function. Think of this as the high-level description of our program.

1#[wasm_bindgen(start)]
2pub fn main() {
3    Program::mount_to_body(App::new());
4}

Model

Data modeling is extremely important in Elm. The point of the mode is to capture all the details about your application as data.

To make a counter, we need to keep track of a number that is going up and down. That means our model is really small this time:

1pub struct App {
2    count: i32,
3}

We just need an i32 value to track the current count. We can see that in out initial value:

1impl App {
2    pub fn new() -> Self {
3        App { count: 0 }
4    }
5}

The initial value is zero, and it will go up and down as people press different buttons.

View

We have a model, but how do we show it on screen? The is the role of the view function:

1impl Application<Msg> for App {
2    fn view(&self) -> Node<Msg> {
3        node! {
4            <main>
5                <input type="button"
6                    value="+"
7                    key="inc"
8                    on_click=|_| {
9                        Msg::Increment
10                    }
11                />
12                <div class="count">{text(self.count)}</div>
13                <input type="button"
14                    value="-"
15                    key="dec"
16                    on_click=|_| {
17                        Msg::Decrement
18                    }
19                />
20            </main>
21        }
22    }
23}

This function takes in the App as an argument. It outputs HTML. So we are saying that we want to show a decrement button, the current count, and an increment button.

Notice that we have an on_click handler for each button. These are saying: when someone clicks, generate a message. So the plus button is generating an Increment message. What is that and where does it go? To the update function!

Update

The update function describes how our Model will change over time. We defined 2 messages that it might receive:

1#[derive(Debug)]
2pub enum Msg {
3    Increment,
4    Decrement,
5}

From there, the update function just describes what to do when you receive one of these messages.

1impl Application<Msg> for App {
2    fn update(&mut self, msg: Msg) -> Cmd<Self, Msg> {
3        match msg {
4            Msg::Increment => self.count += 1,
5            Msg::Decrement => self.count -= 1,
6        }
7        Cmd::none()
8    }
9}

If you get an Increment message, you increment the model. If you get a Decrement message, you decrement the model.

So whenever we get a message, we run it through update to get a new model. We then call the view to figure out how to show the new model on screen. Then repeat! User input generates a message, update the model, view it on screen. Etc.

Overview

Now that you have seen all the parts of a Sauron program, it may be a bit easier to see how they fit into the diagram we saw earlier:

ModelHtmlviewupdateMsgevents

Sauron starts by rendering the initial value on screen. From there you enter into this loop:

  1. Wait for user input.
  2. Send a message to update
  3. Produce a new Model
  4. Call view to get a new HTML
  5. Show the new HTML on screen.
  6. Repeat.

This is the essence of Sauron archicture. Every example we see from now on will be a slight variation on this basic pattern.

Exercise: Add a button to reset the counter to zero:

  1. Add a Reset variant to the Msg type.
  2. Add a Reset branch in the update function
  3. Add a button in the view function.