Creating a Client-Side WebAssembly App Using Yew

In this chapter, you will see how Rust can be used to build the frontend of a web application, as an alternative to using HTML, CSS, and JavaScript (typically using a JavaScript frontend framework, such as React) or another language generating JavaScript code (such as Elm or TypeScript).

To build a Rust app for a web browser, the Rust code must be translated to WebAssembly code, which can be supported by all modern web browsers. The capability to translate Rust code into WebAssembly code is now included in the stable Rust compiler.

To develop large projects, a web frontend framework is needed. In this chapter, the Yew framework will be presented. It is a framework that supports the development of frontend web applications, using the Model-View-Controller (MVC)architectural pattern, and generating WebAssembly code.

The following topics will be covered in this chapter:

  • Understanding the MVC architectural pattern and its usage in web pages
  • Building WebAssembly apps using the Yew framework
  • How to use the Yew crate to create web pages designed with the MVC pattern (incr and adder)
  • Creating a web app having several pages with a common header and footer (login and yauth)
  • Creating a web app having both a frontend and a backend, in two distinct projects (yclient and persons_db)
The frontend is developed using Yew, and the backend, which is an HTTP RESTful service, is developed using Actix web.

Technical requirements

This chapter assumes you have already read the previous chapters, also, prior knowledge of HTML is required.

To run the projects in this chapter, it is enough to install the generator of WebAssembly code (Wasm, for short). Probably the simplest way to do this is by typing the following command:

          cargo install cargo-web
        

After 13 minutes, your Cargo tool will be enriched by several commands. A few of which are as follows:

  • cargo web build (or cargo-web build): It builds Rust projects designed to run in a web browser. It is similar to the cargo build command, but for Wasm.
  • cargo web start (or cargo-web start): It performs a cargo web build command, and then starts a web server where every time it is visited by a client, it sends a complete Wasm frontend app to the client. It is similar to the cargo run command, but for serving Wasm apps.

The complete source code for this chapter is in the Chapter05 folder of the repository at: https://github.com/PacktPublishing/Creative-Projects-for-Rust-Programmers.

Introducing Wasm

Wasm is a powerful new technology to deliver interactive applications. Before the advent of the web, there were already many developers building client/server applications, where the client apps ran on a PC (typically with MicrosoftWindows) and the server apps ran on a company-owned system (typically with NetWare, OS/2, Windows NT, or Unix). In such systems, developers could choose their favorite language for the client app. Some people used Visual Basic, others used FoxPro or Delphi, and many other languages were in wide use.

However, for such systems, the deployment of updates was a kind of hell, because of several possible issues, such as ensuring that every client PC had the proper runtime system and that all clients got the updates at the same time. These problems were solved by JavaScript running in web browsers, as it is a ubiquitous platform on which frontend software could be downloaded and executed. This had some drawbacks though: developers were forced to use HTML + CSS + JavaScript to develop frontend software, and sometimes such software had poor performance.

Here comes Wasm, which is a machine-language-like programming language, like Java bytecode or Microsoft .NET CIL code, but it is a standard accepted by all major web browsers. Version 1.0 of its specification appeared in October 2017, and in 2019 it appears that already more than 80% of web browsers running in the world support it. This means that it can be more efficient and that it can be rather easily generated from several programming languages, including Rust.

So, if Wasm is set as the target architecture of the Rust compiler, a program written in Rust can be run on any major modern web browser.

Understanding the MVC architectural pattern

This chapter is about creating web apps. So, to make things more concrete, let's look straight away attwo toy web applications named incr and adder.

Implementing two toy web apps

To run the first toy application, let's take the following steps:

  1. Go into the incr folder and type cargo web start.
  2. After a few minutes, a message will appear on the console, ending with the following line:
          You can access the web server at `http://127.0.0.1:8000`.
        
  1. Now, in the address box of a web browser, type: 127.0.0.1:8000 or localhost:8000, and immediately you will see the following contents:

  1. Click on the two buttons, or select the following textbox and then press the + or the 0 keys on the keyboard.
  • If you click once on the Increment button, the contents of the box to the right change from 0 to 1.
  • If you click another time, it changes to 2, and so on.
  • If you click on the Reset button, the value changes to 0 (zero).
  • If you select the textbox by clicking on it and then press the + key, you increment the number like the Increment button does. Instead, if you press the 0 key, the number is set to zero.
  1. To stop the server, go to the console and press Ctrl + C.
  2. To run the adder app, go into the adder folder and typecargo web start.
  3. Similarly, for the other app, when the server app has started, you can refresh your web browser page and you will see the following page:

  1. Here, you can insert a number in the first box, to the right of the Addend 1 label, another number in the second box, and then press the Add button. After that, you will see the sum of those numbers in the textbox at the bottom, which has turned from yellow to light green, as in the following screenshot:

After the addition, the Add button has become disabled. If one of the first two boxes is empty, the sum fails and nothing happens. Also, if you change the value of any of the two first boxes, the Add button becomes enabled, and the last textbox becomes empty and yellow.

What is the MVC pattern?

Now that we have seen some very simple web applications, we can explain what the MVC architectural pattern is using these apps as an example. The MVC pattern is an architecture regarding event-driven interactive programs.

Let's see what event-driveninteractiveprograms are. The word interactive is the opposite of batch. A batch program is a program in which the user prepares all the input at the beginning, and then the program runs without asking for further input. Instead, an interactive program has the following steps:

  • Initialization.
  • Waiting for some actions from the user.
  • When the user acts on an input device, the program processes the related input, and then goes to the preceding step, to wait for further input.

For example, console command interpreters are interactive programs, and all web apps are interactive too.

The phrase event-driven means that the application, after initialization, does nothing until the user performs something on the user interface. When the user acts on an input device, the app processes such inputs and updates the screen only as a reaction to the user input. Most web applications are event-driven. The main exceptions are games and virtual reality or augmented reality environments, where animations go on even if the user does nothing.

Our examples in this chapter are all event-driven interactive programs, as after initialization, they do something only when the user clicks with the mouse (or touches the touchscreen) or presses any key on the keyboard. Some such clicks and key presses cause a change on the screen. Therefore, the MVC architecture can be applied to these example projects.

There are several dialects of this pattern. The one used by Yew derives from the one implemented by the Elm language, and so it is named the Elm Architecture.

The model

In any MVC program, there is a data structure, named model, that contains all the dynamic data required to represent the user interface.

For example, in the incr app, the value of the number contained in the box to the right is required to represent the box, and it can change at runtime. Hence, that numeric value must be in the model.

Here, the width and height of the browser window are usually not required to generate the HTML code and so they shouldn't be a part of the model. Also, the sizes and texts of the buttons shouldn't be a part of the model, but for another reason: they cannot change at runtime in this app. Though, if it were an internationalized app, all the texts should be in the model too.

In the adder app, the model should contain only the three values contained in the three textboxes. It doesn't matter that two of them are directly inputted by the user and the third one is calculated. The labels and the background color of the textboxes shouldn't be a part of the model.

The view

The next portion of the MVC architecture is the view. It is a specification of how to represent (or render) the graphical contents of the screen, depending on the value of the model. It can be a declarative specification, such as pure HTML code, or a procedural specification, such as some JavaScript or Rust code, or a mix of them.

For example, in the incr app, the view shows two push-buttons and one read-only textbox, whereas, in the adder app, the view shows three labels, three textboxes, and one push-button.

All the shown push-buttons have a constant appearance, but the views must change the display of the numbers when the models change.

The controller

The last portion of the MVC architecture is the controller. It is always a routine or a set of routines that are invoked by the view when the user, using an input device, interacts with the app. When a user performs an action with an input device, all the view has to do is to notify the controller that the user has performed that action, specifying which action (for example, which mouse key has been pressed), and where (for example, in which position of the screen).

In the incr app, the three possible input actions are as follows:

  • A click on the Increment button
  • A click on the Reset button
  • A press of a key on the keyboard when the textbox is selected

Usually, it is also possible to press a push-button using the keyboard, but such an action can be considered equivalent to a mouse click, and so a single input action type is notified for each button.

In the adder app, the three possible input actions are as follows:

  • A change of the value in the Addend 1 textbox
  • A change of the value in the Addend 2 textbox
  • A click on the Add button

It is possible to change the value of a textbox in several ways:

  • By typing when no text is selected, inserting additional characters
  • By typing when some text is selected, and so replacing the selected text with a character
  • By pasting some text from the clipboard
  • By dragging and dropping some text from another element of the screen
  • By using the mouse on the up-down spinner

We are not interested in these, because they are handled by the browser or by the framework. All that matters for application code is that when the user performs an input action, a textbox changes its value.

The job of the controller is just to use such input information to update the model. When the model is completely updated, the framework notifies the view about the need to refresh the look of the screen, taking into account the new values of the model.

In the case of the incrapp, the controller, when it is notified of the pressing of the Increment button, increments the number contained in the model; when it is notified of the pressing of the Reset button, it sets to zero that number in the model; when it is notified of the pressing of a key on the textbox, it checks whether the pressed key is +, or 0, or something else, and the appropriate change is applied to the model. After such changes, the view is notified to update the display of such a number.

In the case of the adder app, the controller, when it is notified of the change of the Addend 1 textbox, updates the model with the new value contained in the edit box. Similar behavior happens for the Addend 2 textbox; and when the controller is notified of the pressing of the Add button, it adds the two addends contained in the model and stores the result in the third field of the model. After such changes, the view is notified to update the display of such a result.

View implementation

Regarding web pages, the representation of pages is usually made up of HTML code, and so, using the Yew framework, the view function must generate HTML code. Such generations contain in themselves the constant portions of HTML code, but they also access the model to get the information that can change at runtime.

In the incr app, the view composes the HTML code that defines two buttons and one read-only numeric input element and puts in such an input element the value taken from the model. The view includes the handling of the HTML click events on the two buttons by forwarding them to the controller.

In the adder app, the view composes the HTML code that defines three labels, three numeric input elements, and one button, and puts in the last input element the value taken from the model. It includes the handling of the HTML input events in the first two textboxes and the click event on the button, by forwarding them to the controller. Regarding the first two textbox events, the values contained in the boxes are forwarded to the controller.

Controller implementation

Using Yew, the controller is implemented by an update routine, which processes the messages regarding user actions coming from the view and uses such input to change the model. After the controller has completed all the required changes to the model, the view must be notified to apply the changes of the model to the user interface.

In some frameworks, such as in Yew, such an invocation of the view is automatic; that mechanism has the following steps:

  • For any user action handled by the view, the framework calls the update function, that is, the controller. In this call, the framework passes to the controller the details regarding the user action; for example, which value has been typed in a textbox.
  • The controller, typically, changes the state of the model.
  • If the controller has successfully applied some changes to the model, the framework calls the view function, which is the view of the MVC architecture.

Understanding the MVC architecture

The general flow of control of the MVC architecture is shown in the following diagram:

The iteration of every user action is this sequence of operations:

  1. The user sees a static representation of graphical elements on the screen.
  2. The user acts on the graphical elements using an input device.
  3. The view receives a user action and notifies the controller.
  4. The controller updates the model.
  5. The view reads the new state of the model to update the contents of the screen.
  6. The user sees the new state of the screen.

The main concepts of the MVC architecture are as follows:

  • All the mutable data that is needed to correctly build the displaymust be in a single data structure, named model. The model may be associated with some code, but such code does not get direct user input, nor does it give output to the user. It may access files, databases, or other processes, though. Because the model does not interact directly with the user interface, the code implementing the model shouldn't change if the application user interface is ported from text mode to GUI/web/mobile.
  • The logic that draws on the display and captures user input is named the view. The view, of course, must know about screen rendering, input devices and events, and also about the model. Though, the view just reads the model, it never changes it directly. When an interesting event happens, the view notifies the controller of that event.
  • When the controller is notified of an interesting event by the view, it changes the model accordingly, and when it has finished, the framework notifies the view to refresh itself using the new state of the model.

Project overview

This chapter will present four projects that will get more and more complex. You have already seen the first two projects in action: incr and adder. The third project, named login, shows how to create a login page for authentication on a website.

The fourth project, named yauth, extends the login project adding the CRUD handling of a list of persons. Its behavior is almost identical to that of the auth project in Chapter 4, Creating a Full Server-Side Web App. Each project will require from 1 to 3 minutes to download and compile from scratch.

Getting started

To start all the machinery, a very simple statement is enough – the body of themainfunction:

 yew::start_app::<Model>();

It creates a web app based on the specifiedModel, starts it, and waits on the default TCP port. Of course, the TCP port can be changed. It is a server that will serve the app to any browser navigating to it.

The incr app

Here, we'll see the implementation of the incr project, which we already saw how to build and use. The only dependency is on the Yew framework, and so, the TOML file contains the following line:

yew = "0.6"

All the source code is in the main.rs file. The model is implemented by the following simple declaration:

struct Model {
value: u64,
}

It just has to be a struct that will be instantiated by the framework, read by the view, and read and written by the controller. Its name and the name of its fields are arbitrary.

Then the possible notifications from the view to the controller must be declared as an enum type. Here is that of incr:

enum Msg {
Increment,
Reset,
KeyDown(String),
}

Also, here, the names are arbitrary:

  • Msg is short for message, as such notifications are in a sense messages from the view to the controller.
  • The Incrementmessage notifies a click on the Increment button.TheResetmessage notifies a click on the Reset button.
  • TheKeyDownmessage notifies a press of any key on the keyboard; its argument communicates which key has been pressed.

To implement the controller, the yew::Component trait must be implemented for our model. The code for our project is as follows:

impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { value: 0 }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender { ... }
}

The required implementations are as follows:

  • Message: It is the enum defined before, describing all possible notifications from the view to the controller.
  • Properties: It is not used in this project. When not used, it must be an empty tuple.
  • create: It is invoked by the framework to let the controller initialize the model. It can use two arguments, but here we are not interested in them, and it must return an instance of the model with its initial value. As we want to show the number zero at the beginning, we set value to 0.
  • update: It is invoked by the framework any time the user acts on the page in some way handled by the view. The two arguments are the mutable model itself (self) and the notification from the view (msg). This method should return a value of type ShouldRender, but a bool value will be good. Returning true means that the model has been changed, and so a refresh of the view is required. Returning falsemeans that the model has not been changed, and so a refresh of the view would be a waste of time.

The update method contains a match on the message type. The first two message types are quite simple:

match msg {
Msg::Increment => {
self.value += 1;
true
}
Msg::Reset => {
self.value = 0;
true
}

If the Increment message is notified, the value is incremented. If the Reset message is notified, the value is zeroed. In both cases, the view must be refreshed.

The handling of the keypress is a bit more complex:

Msg::KeyDown(s) => match s.as_ref() {
"+" => {
self.value += 1;
true
}
"0" => {
self.value = 0;
true
}
_ => false,
}

The KeyDown match arm assigns the key pressed to the s variable. As we are interested only in two possible keys, there is a nested match statement on the s variable. For the two-handled keys (+ and 0), the model is updated, and true is returned to refresh the view. For any other key pressed, nothing is done.

To implement the view part of MVC, the yew::Renderable trait must be implemented for our model. The only required method is view, which gets an immutable reference to the model, and returns an object that represents some HTML code, but that is capable of reading the model and notifying the controller:

impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! { ... }
}
}

The body of such a method is constructed with the powerful yew::html macro. Here is the body of such a macro invocation:

<div>
<button onclick=|_| Msg::Increment,>{"Increment"}</button>
<button onclick=|_| Msg::Reset,>{"Reset"}</button>
<input
readonly="true",
value={self.value},
onkeydown=|e| Msg::KeyDown(e.key()),
/>
</div>

It looks very similar to the actual HTML code. It is equivalent to the following HTML pseudo-code:

<div>
<button onclick="notify(Increment)">Increment</button>
<button onclick="notify(Reset)">Reset</button>
<input
readonly="true"
value="[value]"
onkeydown="notify(KeyDown, [key])"),
/>
</div>

Notice that at any HTML event, in the HTML pseudo-code, a JavaScript function is invoked (here, named notify). Instead, in Rust, there is a closure that returns a message for the controller. Such a message must have the arguments of the appropriate type. While the onclick event has no arguments, the onkeydown event has one argument, captured in the e variable, and by calling the key method on that argument, the pressed key is passed to the controller.

Also notice in the HTML pseudo-code the [value] symbol, which at runtime will be replaced by an actual value.

Finally, notice that the body of the macro has three features that differentiate it from HTML code:

  • All the arguments of HTML elements must end with a comma.
  • Any Rust expression can be evaluated inside HTML code, as long as it is enclosed in braces.
  • Literal strings are not allowed in this HTML code, so they must be inserted as Rust literals (by including them in braces).

The adder app

Here, we'll see the implementation of the adder project, which we already saw how to build and use. Only that which differentiates it from the incr project will be examined.

First of all, there is a problem with the html macro expansion recursion level. It is so deep that it must be increased using the following directives at the beginning of the program:

#![recursion_limit = "128"]
#[macro_use]
extern crate yew;

Without them, a compilation error is generated. With more complex views, an even larger limit is required. The model contains the following fields:

addend1: String,
addend2: String,
sum: Option<f64>,

They represent the following, respectively:

  • The text inserted in the first box (addend1).
  • The text inserted in the second box (addend2).
  • The number calculated and to be displayed in the third box, if the calculation was performed and was successful, or nothing otherwise.

The handled events (that is, the messages) are as follows:

 ChangedAddend1(String),
ChangedAddend2(String),
ComputeSum,

They represent the following, respectively:

  • Any change to the contents of the first box, with the new value contained in the box (ChangedAddend1).
  • Any change to the contents of the second box, with its value (ChangedAddend2).
  • A click on the Add button.

The create function initializes the three fields of the model: the two addends are set to empty strings, and the sum field is set to None. With these initial values, no number is displayed in the Sum textbox.

The update function processes the three possible messages. For the ComputeSum message, it does the following:

self.sum = match (self.addend1.parse::<f64>(), self.addend2.parse::<f64>()) {
(Ok(a1), Ok(a2)) => Some(a1 + a2),
_ => None,
};

The addend1 and addend2 fields of the model are parsed to convert them into numbers. If both conversions are successful, the first arm matches, and so the a1 and a2 values are added, and their sum is assigned to the sum field. If some conversion fails, None is assigned to the sum field.

The arm regarding the first addend is as follows:

Msg::ChangedAddend1(value) => {
self.addend1 = value;
self.sum = None;
}

The current value of the textbox is assigned to the addend1 field of the model, and the sum field is set to None. Similar behavior is performed for a change to the other addend.

Let's see the most interesting parts of the view method:

 let numeric = "text-align: right;";

It assigns to a Rust variable a snippet of CSS code. Then, the textbox for the first addend is created by the following code:

<input type="number", style=numeric,
oninput=|e| Msg::ChangedAddend1(e.value),/>

Notice that to the style attribute, the value of the numeric variable is assigned. The values of these attributes are just Rust expressions.

The sum textbox is created by the following code:

<input type="number",

style=numeric.to_string()
+ "background-color: "
+ if self.sum.is_some() { "lightgreen;" } else { "yellow;" },
readonly="true", value={
match self.sum { Some(n) => n.to_string(), None => "".to_string() }
},
/>

The style attribute is composed by concatenating the numeric string seen before with the background color. Such a color is light green if sum has a numeric value, or yellow if it is None. Also, the value attribute is assigned using an expression, to assign an empty string if sum is None.

Thelogin app

So far, we have seen that an app contains just one model struct, one enum of messages, one create function, one updatemethod, and one view method. This is good for very simple apps, but with more complex apps, this simple architecture becomes unwieldy. There is a need to separate different portions of the app in different components, where each component is designed with the MVC pattern and so it has its own model, controller, and view.

Typically, but not necessarily, there is a general component that contains the portions of the app that remain the same for all of the app:

  • A header with a logo, a menu, and the name of the current user
  • A footer containing copyright information and contact information

And then in the middle of the page, there is the inner part (also named the body, although it is not the body HTML element). This inner part contains the real information of the app and is one of many possible components or forms (or pages):

  1. Let's run the login app by typing cargo web start in its folder.
  2. When navigating to localhost:8000, the following page appears:

There are two horizontal lines. The part above the first line is meant to be a header, which must remain for the whole app. The part underneath the second line is meant to be a footer, which must remain for the whole app, too. The median part is the Login component, which appears only when the user must be authenticated. This portion will be replaced by other components when the user is authenticated.

First of all, let's see some authentication failures:

  • If you click on Log in straightaway, a message box appears saying: User not found. The same happens if you type some random characters in the User name textbox. The only allowed user names are susan and joe.
  • If you insert one of the two allowed user names, and then you click on Log in, you get the message Invalid password for the specified user.
  • The same happens if you type some random characters in the Password textbox. The only allowed passwords are xsusan for the user susan, and xjoe for the user joe. If you type susan and then xsusan, just before clicking on Log in, you will see the following:

And just after, you will see the following:

Three things have changed:

  • At the right of the label—Current user—the blue text --- has been replaced by susan.
  • At the right of that blue text, the Change Userbutton has appeared.
  • Between the two horizontal lines, all the HTML elements have been replaced by the large text reading Page to be implemented. Of course, this situation would represent a case in which the user has been successfully authenticated and is using the rest of the app.

If you were to click the Change User button, you will get the following page:

It is similar to the first page, but the name susan appears both as Current user, and as User name.

Organization of the project

The source code of this project has been split into three files (which you will find in the book's GitHub repository at Chapter05/login/src/db_access.rs:

  • db_access.rs: Contains a stub of a user directory to handle authentication
  • main.rs: Contains the one-line main function, and an MVC component that handles the header and the footer of the page, and delegates the inner section to the authentication component
  • login.rs: Contains the MVC component to handle the authentication, to be used as an inner section of the main component

The db_access.rs file

The db_access module is a subset of that of the previous chapter. It declares a DbConnection struct that simulates a connection to a database. Actually, for simplicity, it contains just Vec<User>, where User is an account of the app:

#[derive(PartialEq, Clone)]
pub struct DbConnection {
users: Vec<User>,
}

The definition of the User type is this:

pub enum DbPrivilege {
CanRead,
CanWrite,
}

pub struct User {
pub username: String,
pub password: String,
pub privileges: Vec<DbPrivilege>,
}

Any user of the app has a name, a password, and some privileges. In this simple system, there are only two possible privileges:

  • CanRead, which means that the user can read all of the database
  • CanWrite, which means that the user can change all of the database (that is, inserting, updating, and deleting records)

Two users are wired in:

  • joe with the password xjoe, capable only of reading from the database
  • susan with the password xsusan, capable of reading and writing the data

The only functions are as follows:

  • new, to create a DbConnection:
pub fn new() -> DbConnection {
DbConnection {
users: vec![
User {
username: "joe".to_string(),
password: "xjoe".to_string(),
privileges: vec![DbPrivilege::CanRead],
},
User {
username: "susan".to_string(),
password: "xsusan".to_string(),
privileges: vec![DbPrivilege::CanRead,
DbPrivilege::CanWrite],
},
],
}
}
  • get_user_by_username, to get a reference to the user having the specified name, or None if there is no user with that name:
pub fn get_user_by_username(&self, username: &str) -> Option<&User> {
if let Some(u) = self.users.iter().find(|u|
u.username == username) {
Some(u)
} else {
None
}
}

Of course, first, we will create a DbConnection object, using the new function, and then we will get a User from that object, using the get_user_by_username method.

The main.rs file

The main.rs file begins with the following declarations:

mod login;

enum Page {
Login,
PersonsList,
}

The first declaration imports the login module, which will be referenced by the main module. Any inner section module must be imported here.

The second statement declares all the components that will be used as inner sections. Here, we have only the authentication component (Login) and a component that is not yet implemented (PersonsList).

Then, there is the model of the MVC component of the main page:

struct MainModel {
page: Page,
current_user: Option<String>,
can_write: bool,
db_connection: std::rc::Rc<std::cell::RefCell<DbConnection>>,
}

As a convention, the name of any model ends with Model:

  • The first field of the model is the most important one. It represents which inner section (or page) is currently active.
  • The other fields contain global information, that is, information useful for displaying the header, the footer, or that must be shared with the inner components.
  • The current_user field contains the name of the logged-in user, orNone if no user is logged in.
  • Thecan_write flag is a simplistic description of user privileges; here, both users can read, but only one can also write, and so this flag istruewhen they are logged in.
  • The db_connection field is a reference to the database stub. It must be shared with an inner component, and so it is implemented as a reference-counted smart pointer toRefCell, containing the actualDbConnection. Using this wrapping, any object can be shared with other components, as long as one thread at a time accesses them.

The possible notifications from the view to the controller are these:

enum MainMsg {
LoggedIn(User),
ChangeUserPressed,
}

Remember that the footer has no elements that can get input, and for the header, there is only the Change User button that can get input, when it is visible. By pressing such a button, the ChangeUserPressed message is sent.

So, it appears there is no way to send the LoggedIn message! Actually, the Login component can send it to the main component.

The update function of the controller has the following body:

match msg {
MainMsg::LoggedIn(user) => {
self.page = Page::PersonsList;
self.current_user = Some(user.username);
self.can_write = user.privileges.contains(&DbPrivilege::CanWrite);
}
MainMsg::ChangeUserPressed => self.page = Page::Login,

When the Login component notifies the main component of successful authentication, thus specifying the authenticated user, the main controller sets PersonsList as the page to go to, saves the name of the newly authenticated user, and extracts the privileges from that user.

When the Change User button is clicked, the page to go to becomes the Login page. The view method contains just an invocation of the html macro. Such a macro must contain one HTML element, and in this case, it is a div element.

That div element contains three HTML elements: a style element, a header element, and a footer element. But between the header and the footer, there is some Rust code to create the inner section of the main page.

To insert Rust code inside an html macro, there are two possibilities:

  • Attributes of HTML elements are just Rust code.
  • At any point, a pair of braces encloses Rust code.

In the first case, the evaluation of such Rust code must return a value convertible to a string through the Display trait.

In the second case, the evaluation of the Rust code in braces must return an HTML element. And how can you return an HTML element from Rust code? Using an html macro!

So, the Rust code that implements the view method contains an html macro invocation that contains a block of Rust code, which contains an html macro invocation, and so on. This recursion is performed at compile time and has a limit that can be overridden using the recursion_limit Rust attribute.

Notice that both the header and the inner section contain a match self.page expression.

In the header, it is used to show the Change User button only if the current page is not the login page, for which it would be pointless.

In the inner section, the body of such a statement is the following:

Page::Login => html! {
<LoginModel:
current_username=&self.current_user,
when_logged_in=|u| MainMsg::LoggedIn(u),
db_connection=Some(self.db_connection.clone()),
/>
},
Page::PersonsList => html! {
<h2>{ "Page to be implemented" }</h2>
},

If the current page is Login, an invocation to the html macro contains the LoginModel: HTML element. Actually, the HTML language doesn't have such an element type. This is the way to embed another Yew component in the current component. The LoginModel component is declared in the login.rs source file. Its construction requires some arguments:

  • current_username is the name of the current user.
  • when_logged_in is a callback that the component should invoke when it has performed a successful authentication.
  • db_connection is a (reference-counted) copy of the database.

Regarding the callback, notice that it receives a user (u) as an argument and returns the message LoggedIn decorated by that user. Sending this message to the controller of the main component is the way the Login component communicates to the main component who the user is that has just logged in.

Thelogin.rs file

The login module begins by defining the model of the Login component:

pub struct LoginModel {
dialog: DialogService,
username: String,
password: String,
when_logged_in: Option<Callback<User>>,
db_connection: std::rc::Rc<std::cell::RefCell<DbConnection>>,
}

This model must be used by the main component, and so it must be public.

Its fields are as follows:

  • dialog is a reference to a Yew service, which is a way to ask the framework to do something more than implementing the MVC architecture. A dialog service is the ability to show message boxes to the user, through the JavaScript engine of the browser.
  • username and password are the values of the text that the user has typed in the two textboxes.
  • when_logged_in is a possible callback function, to call when a successful authentication is completed.
  • db_connection is a reference to the database.

The possible notification messages are these:

pub enum LoginMsg {
UsernameChanged(String),
PasswordChanged(String),
LoginPressed,
}

The first two messages mean that the respective fields have changed values, and the third message says that the push-button has been pressed.

So far, we have seen that this component has a model and some messages, like the components we saw before; but now we'll see that it also has something that we've never seen:

pub struct LoginProps {
pub current_username: Option<String>,
pub when_logged_in: Option<Callback<User>>,
pub db_connection:
Option<std::rc::Rc<std::cell::RefCell<DbConnection>>>,
}

This structure represents the arguments that every parent of this component must pass to create the component. In this project, there is only one parent of the Login component, that is, the main component, and that component created a LoginModel: element having the fields of LoginProps as attributes. Notice that all the fields are specializations of Option: it is required by the Yew framework, even if you don't pass an Option as an attribute.

This LoginProps type must be used in four points:

  • First, it must implement the Default trait, to ensure its fields are properly initialized when the framework needs an object of this type:
impl Default for LoginProps {
fn default() -> Self {
LoginProps {
current_username: None,
when_logged_in: None,
db_connection: None,
}
}
}
  • Second, we already saw that the implementation of the Component trait for the model has to define a Properties type. In this case, it must be like so:
impl Component for LoginModel {
type Message = LoginMsg;
type Properties = LoginProps;
That is, this type is passed into the implementation of the Component trait for the LoginModel type.
  • Third, the create function must use its first argument, containing the values passed in by the parent component. Here is that function:
fn create(props: Self::Properties, _link: ComponentLink<Self>)
-> Self {
LoginModel {
dialog: DialogService::new(),
username: props.current_username.unwrap_or(String::new()),
password: String::new(),
when_logged_in: props.when_logged_in,
db_connection: props.db_connection.unwrap(),
}
}

All the fields of the model are initialized, but while the dialog and password fields receive default values, the other fields receive a value from the props object received from the parent component, that is, MainModel. As we are sure that the db_connection field of props will be None, we call unwrap for it. Instead, the current_username field may be None, and so, in that case, an empty string is used.

Then there is the update function, which is the controller of the Login component.

When the user presses the Log in button, the following code is executed:

if let Some(user) = self.db_connection.borrow()
.get_user_by_username(&self.username)
{
if user.password == self.password {
if let Some(ref go_to_page) = self.when_logged_in {
go_to_page.emit(user.clone());
}
} else {
self.dialog.alert("Invalid password for the specified user.");
}
} else {
self.dialog.alert("User not found.");
}

The connection to the database is extracted from RefCell using the borrow method, and then the user with the current name is looked for. If the user is found, and if their stored password is the same as that typed by the user, the callback kept in the when_logged_in field is extracted, and then its emit method is invoked, passing a copy of the user name as argument. So, the routine passed by the parent, that is, the |u| MainMsg::LoggedIn(u) closure, is executed.

In the event of a missing user or mismatching password, a message box is displayed using the alert method of the dialog service. The controllers that we saw before had just two functions: create and update. This one has another function, though; it is the change method:

fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.username = props.current_username.unwrap_or(String::new());
self.when_logged_in = props.when_logged_in;
self.db_connection = props.db_connection.unwrap();
true
}

This method allows the parent to re-send to this component updated arguments using the Properties structure. The create method is invoked just one time, while the change method is invoked any time the parent will need to update the arguments to pass to the child component.

The view is easy to understand by reading its code and does not require explanation.

The yauth app

The login app, presented in the previous section, showed how to create a parent component containing one of several possible child components. However, it implemented just one child component, the Login component. So, in this section, a more complete example will be presented, having three different possible child components, corresponding to three different pages of a classical web application.

It is named yauth, short for Yew Auth, as its behavior is almost identical to the auth project shown in the previous chapter, although, it is completely based on the Yew framework, instead of being based on Actix web and Tera.

Understanding the behavior of the app

This app is built and launched like the ones in the previous sections, and its first page is identical to the first page of the login app. Though, if you type susan as the username and xsusan as the password, and then click on the Log in button, you'll see the following page:

This page and the other page that you will see in this app, and their behavior, are almost identical to those of the auth app described in the previous chapter. The only differences are as follows:

  • Any error message is not shown as red text embedded in the page but as a pop-up message box.
  • The header and the footer are implemented by the main component, and they look and behave as already described in the previous section of this chapter.

So, we just need to examine the implementation of this app.

Organization of the project

The source code of this project has been split into five files:

  • db_access.rs: It contains a stub of a connection to a database, providing access to a user directory to handle authentication and to a list of persons; it actually contains such data as vectors. It is virtually identical to the file with the same name in the auth project of the previous chapter. The only relevant difference is that the Serialize trait is not implemented, because it's not required by the Yew framework.
  • main.rs: It contains the one-linemainfunction, and an MVC component that handles the header and the footer of the page, and delegates the inner section to one of the other three components of the app.
  • login.rs: It contains the MVC component to handle the authentication. It is to be used as an inner section of the main component. It is identical to the module having the same name in the login project.
  • persons_list.rs: It contains the MVC component to handle the list of persons. It is to be used as an inner section of the main component.
  • one_person.rs: It contains the MVC component to view, edit, or insert a single person; it is to be used as an inner section of the main component.

We will only discuss the files unique to the yauth app, as follows.

Thepersons_list.rs file

This file contains the definition of the component to let the user manage the list of persons, and so it defines the following struct as a model:

pub struct PersonsListModel {
dialog: DialogService,
id_to_find: Option<u32>,
name_portion: String,
filtered_persons: Vec<Person>,
selected_ids: std::collections::HashSet<u32>,
can_write: bool,
go_to_one_person_page: Option<Callback<Option<Person>>>,
db_connection: std::rc::Rc<std::cell::RefCell<DbConnection>>,
}

Let's see what each line in the previous code says:

  • The dialog field contains a service to open message boxes.
  • The id_to_findfield contains the value typed by the user in the Id textbox if the box contains a number, orNoneotherwise.
  • The name_portionfield contains the value contained in theName portion:textbox. In particular, if that box is empty, this field of the model contains an empty string. The filtered_personsfield contains a list of the persons extracted from the database using the specified filter. Initially, the filter specifies to extract all the persons whose names contain an empty string. Of course, all the persons satisfy that filter, and so all the persons in the database are added to this vector, though the database is empty, and so this vector is too.
  • The selected_ids field contains the IDs of all the listed people whose checkbox is set, and so they are selected for further operation.
  • The can_write field specifies whether the current user has the privilege to modify the data.
  • The go_to_one_person_pagefield contains the callback to call to pass to the page to view/edit/insert a single person. Such a callback function receives one argument, which is the person to view/edit, orNone to open the page to insert a new person.
  • The db_connectionfield contains a shared reference to the database connection.

The possible notifications from the view to the controllers are defined by this structure:

pub enum PersonsListMsg {
IdChanged(String),
FindPressed,
PartialNameChanged(String),
FilterPressed,
DeletePressed,
AddPressed,
SelectionToggled(u32),
EditPressed(u32),
}

Let's see what we did in the previous code:

  • The IdChanged message must be sent when the text in the Id: textbox is changed. Its argument is the new text value of the field.
  • The FindPressed message must be sent when the Find push-button is clicked.
  • The PartialNameChanged message must be sent when the text in the Name portion: textbox is changed. Its argument is the new text value of the field.
  • The FilterPressed message must be sent when the Filter push-button is clicked.
  • The DeletePressed message must be sent when the Delete Selected Persons push-button is clicked.
  • The AddPressed message must be sent when the Add New Person push-button is clicked.
  • The SelectionToggled message must be sent when a checkbox in the list of persons is toggled (that is, checked or unchecked). Its argument is the ID of the person specified by that line of the list.
  • The EditPressed message must be sent when any Edit push-button in the list of persons is clicked. Its argument is the ID of the person specified by that line of the list.

Then, the structure of the initialization arguments for the component is defined:

pub struct PersonsListProps {
pub can_write: bool,
pub go_to_one_person_page: Option<Callback<Option<Person>>>,
pub db_connection:
Option<std::rc::Rc<std::cell::RefCell<DbConnection>>>,
}

Let's look at how this works:

  • Using the can_write field, the main component specifies a simple definition of the privileges of the current user. A more complex application could have a more complex definition of privileges.
  • Using the go_to_one_person_page field, the main component passes a reference to a function, which must be called to go to the page for showing, editing, or inserting a single person.
  • Using the db_connection field, the main component passes a shared reference to the database connection.

The initialization of the PersonsListProps struct by implementing the Default trait and of the PersonsListModel struct by implementing the Component trait is trivial, except for the filtered_persons field. Instead of leaving it as an empty vector, it is first set as an empty vector, and then modified by the following statement:

model.filtered_persons = model.db_connection.borrow()
.get_persons_by_partial_name("");

Why an empty collection wouldn't be good for filtered_persons

Every time the PersonsList page is opened, both from the login page and from the OnePerson page, the model is initialized by the create function, and all the user interface elements of the page are initialized using that model.

So, if you type something in the PersonsList page, and then you go to another page, and then you go back to the PersonsList page, everything you typed is cleared unless you set it in the create function.

Probably, the fact that the Idtextbox, theName portion textbox, or the selected persons are cleared is not very annoying, but the fact that the list of persons is cleared means that you will get the following behavior:

  • You filter the persons to see some persons listed.
  • You click on the Edit button in the row of one person, to change the name of that person, and so you go to the OnePerson page.
  • You change the name and press the Update button, and so you go back to the PersonsList page.
  • You see the text No persons. instead of the list of persons.

You don't see the person that you have just modified in the OnePerson page anymore. This is inconvenient.

To see that person listed, you need to set filtered_persons to a value containing that person. The solution chosen has been to show all the persons existing in the database, and this is performed by calling the get_persons_by_partial_name("") function.

Now, let's see how the update method handles the messages from the view.

When the IdChangedmessage is received, the following statement is executed:

self.id_to_find = id_str.parse::<u32>().ok(),

It tries to store in the model the value of the textbox, or None if the value is not convertible to a number.

When the FindPressedmessage is received, the following statement is executed:

match self.id_to_find {
Some(id) => { self.update(PersonsListMsg::EditPressed(id)); }
None => { self.dialog.alert("No id specified."); }
},

If the Id textbox contained a valid number, another message would be sent recursively: it is the EditPressed message. Pressing the Find button must have the same behavior as pressing the Edit button in the row with the same ID contained in the Id textbox, and so the message is forwarded to the same function. If there is no ID in the text field, a message box is displayed.

When the PartialNameChanged message is received, the new partial name is just saved in the name_portion field of the model. When the FilterPressed message is received, the following statement is executed:

self.filtered_persons = self
.db_connection
.borrow()
.get_persons_by_partial_name(&self.name_portion);

The connection to the database is encapsulated in a RefCell object, which is further encapsulatedin an Rc object. The access inside Rc is implicit, but to access inside RefCell, it is required to call the borrow method. Then the database is queried to get the list of all the persons whose names contain the current name portion. This list is finally assigned to the filtered_persons field of the model.

When the DeletePressed message is received, the following statement is executed:

if self
.dialog
.confirm("Do you confirm to delete the selected persons?") {
{
let mut db = self.db_connection.borrow_mut();
for id in &self.selected_ids {
db.delete_by_id(*id);
}
}
self.update(PersonsListMsg::FilterPressed);
self.dialog.alert("Deleted.");
}

The following pop-up box is shown for confirmation:

If the user clicks on the OK button (or presses Enter), then the deletion is performed in the following way: a mutable reference is borrowed from the shared connection to the database, and for any ID selected through the checkboxes, the respective person is deleted from the database.

The closing of the scope releases the borrowing. Then, a recursive call to update triggers the FilterPressed message, whose purpose is to refresh the list of persons shown. Finally, the following message box communicates the completion of the operation:

When the AddPressed message is received, the following code is executed:

if let Some(ref go_to_page) = self.go_to_one_person_page {
go_to_page.emit(None);
}

Here, a reference to the go_to_one_person_page callback is taken, and then it is invoked using the emit method. The effect of such an invocation is to go to the OnePerson page. The argument of emit specifies which person will be edited on the page. If it is None, as in this case, the page is opened in insertion mode.

When the SelectionToggled message is received, it specifies an ID of a person, but it does not specify whether that person is to be selected or deselected. So, the following code is executed:

if self.selected_ids.contains(&id) {
self.selected_ids.remove(&id);
} else {
self.selected_ids.insert(id);
}

We want to invert the status of the person on which the user has clicked, that is, to select it if it was not selected, and to unselect it if it was selected. The selected_ids field of the model contains the set of all the selected persons. So, if the clicked ID is contained in the set of selected IDs, it is removed from this set by calling the remove method; otherwise, it is added to the list, by calling the insert method.

At last, when the EditPressed message is received (specifying the id of the person to view/change), the following code is executed:

match self.db_connection.borrow().get_person_by_id(id) {
Some(person) => {
if let Some(ref go_to_page) = self.go_to_one_person_page {
go_to_page.emit(Some(person.clone()));
}
}
None => self.dialog.alert("No person found with the indicated id."),
}

The database is searched for a person with the specified ID. If such a person is found, the go_to_one_person_page callback is invoked, passing a clone of the person found. Otherwise, a message box explains the error. The change method keeps the fields of the model updated when any property coming from the parent component would change.

Then there is the view. The messages sent by the view were described when the messages were presented. The other interesting aspects of the view are the following ones.

The Delete Selected Persons button and the Add New Person button have the attribute disabled=!self.can_write. This enables such commands only if the user has the privilege to change the data.

The if !self.filtered_persons.is_empty()clause causes the table of persons to be displayed only if there is at least one person filtered. Otherwise, the text No persons. is displayed.

The body of the table begins and ends with the following lines:

for self.filtered_persons.iter().map(|p| {
let id = p.id;
let name = p.name.clone();
html! {
...
}
})

This is the required syntax for generating sequences of HTML elements based on an iterator.

The for keyword is immediately followed by an iterator (in this case, the expression self.filtered_persons.iter()), followed by the expression .map(|p|, where p is the loop variable. In this way, it is possible to insert into the map closure a call to the html macro that generates the elements of the sequence. In this case, such elements are the lines of the HTML table.

The last noteworthy point is the way to show which persons are selected. Every checkbox has the attribute checked=self.selected_ids.contains(&id),. The checked attribute expects a bool value. That expression sets as checked the checkbox relative to the persons whose id is contained in the list of the selected IDs.

Theone_person.rs file

This file contains the definition of the component to let the user view or edit the details of one person or to fill in the details and insert a new person. Of course, to view the details of an existing record, such details must be passed as arguments to the component; instead, to insert a new person, no data must be passed to the component.

This component does not return its changes directly to the parent that created it. Such changes are saved to the database, if the user requested that, and the parent can retrieve them from the database.

Therefore the model is defined by the following struct:

pub struct OnePersonModel {
id: Option<u32>,
name: String,
can_write: bool,
is_inserting: bool,
go_to_persons_list_page: Option<Callback<()>>,
db_connection: std::rc::Rc<std::cell::RefCell<DbConnection>>,
}

With the preceding code, we understood the following things:

  • The idfield contains the value contained in the Id textbox if the box contains a number, orNoneotherwise.
  • The namefield contains the value contained in the Name textbox. In particular, if the box is empty, this field of the model contains an empty string.
  • The can_writefield specifies whether the current privileges allow the user to change the data or only to see it.
  • The is_inserting field specifies whether this component has received no data, to insert a new person into the database, or whether it has received the data of a person, to view or edit them.
  • The go_to_persons_list_page field is a callback with no arguments that must be invoked by this component when the user closes this page to go to the page to manage the list of persons.
  • The db_connection field is a shared connection to the database.

Of course, it is pointless to open a page for insertion without allowing the user to change the values. So, the possible combinations are the following ones:

  • Insertion mode: The id field is None, the can_write field is true, and the is_inserting field is true.
  • Editing mode: The id field is Some, the can_write field is true, and the is_inserting field is false.
  • Read-only mode: The id field is Some, the can_write field is false, and the is_inserting field is false.

The possible notifications from the view to the controller are defined by the following enum:

pub enum OnePersonMsg {
NameChanged(String),
SavePressed,
CancelPressed,
}

Let's see what happened in the code:

  • When the user changes the contents of the Nametextbox, the NameChanged message is sent, which also specifies the current contents of that textbox.
  • When the user clicks on the Insert button or on the Update button, the SavePressed message is sent. To distinguish between the two buttons, theis_inserting field can be used.
  • When the user presses the Cancel button, the CancelPressed message is sent.

The value of the Id textbox can never be changed during the life of this component, and so no message is required for it. The data received from the parent is defined by the following structure:

pub struct OnePersonProps {
pub id: Option<u32>,
pub name: String,
pub can_write: bool,
pub go_to_persons_list_page: Option<Callback<()>>,
pub db_connection:
Option<std::rc::Rc<std::cell::RefCell<DbConnection>>>,
}

In the preceding code, we have the following things to check:

  • The id field is None in case the parent wants to open the page to let the user insert a new person, and contains the ID of an existing person in case the page is for viewing or editing the data of that person.
  • The name field is the only changeable data of any person. It is an empty string if the page is created for inserting a new person. Otherwise, the parent passes the current name of the person.
  • The can_write field specifies whether the user is allowed to change the displayed data. This field should be true if the idfield isNone.
  • go_to_persons_list_page is the callback that will activate the PersonsList component in the parent.
  • The db_connection field is the shared database connection.

In the rest of the module, there is nothing new. The only thing to stress is that the use of conditional expressions based on the can_write and is_inserting flags of the model allows having just one component with a mutant view.

A web app accessing a RESTful service

The previous section described a rather complex software architecture, but still running only in the user's web browser, after having being served by the site where it is installed. This is quite unusual, as most web apps actually communicate with some other process. Typically, the same site that provides the frontend app also provides a backend service, that is, a web service to let the app access shared data residing on the server.

In this section, we'll see a pair of projects that can be downloaded from the repository:

  • yclient: This is an app quite similar to the yauth app. Actually, it is developed using Yew and Wasm, and it has the same look and behavior as yauth; though its data, which is the authorized users and the persons stored in the mock database, no longer resides in the app itself, but in another app, which is accessed through an HTTP connection.
  • persons_db: This is the RESTful service that provides access to the data for the yclient app. It is developed using the Actix web framework, as explained in the previous chapter. Even this app does not manage a real database, only a mock, in-memory database.

To run the system, two commands are required: one to run the frontend provider, yclient, and one to run the web service, persons_db.

To run the frontend provider, go into the yclient folder, and type the following:

          cargo web start
        

After downloading and compiling all the required crates, it will print the following:

          You can access the web server at `http://127.0.0.1:8000`.
        

To run the backend, in another console window, go into the db_persons folder and type the following:

          cargo run
        

Or, we can use the following command:

          cargo run --release
        

Both these commands will end by printing the following:

           Listening at address 127.0.0.1:8080
        

Now you can use your web browser and navigate to localhost:8000. The app that will be opened will be quite similar to both the yauth app, shown in the previous section, and to the auth app, shown in the previous chapter.

Let's first see how persons_db is organized.

The persons_db app

This app uses the Actix web framework, described in the previous two chapters. In particular, this project has some features taken from the json_db project, described in Chapter 3, Creating a REST Web Service, and some from the auth project, described in Chapter 4, Creating a Full Server-Side Web App.

Here, we'll see only the new features that haven't been described so far. The Cargo.toml file contains the following new line:

actix-cors = "0.1"

This crate allows the handling of the Cross-Origin Resource Sharing (CORS) checks, usually performed by browsers. When some code running inside a browser tries to access an external resource using a network connection, the browser, for security reasons, checks whether the addressed host is just the one that provided the code that is performing the request. That means that the frontend and the backend are actually the same website.

If the check fails, that is, the frontend app is trying to communicate with a different site, the browser sends an HTTP request using the OPTION method to check whether the site agrees to cooperate with that web app on this resource sharing. Only if the response to the OPTION request allows the required kind of access can the original request be forwarded.

In our case, both the frontend app and the web service run on localhost; though, they use different TCP ports: 8000 for the frontend and 8080 for the backend. So, they are considered as different origins, and CORS handling is needed. The actix-cors crate provides features to allow such cross-origin access for backends developed using Actix web.

One of these features is used in the main function, as in the following code snippet:

.wrap(
actix_cors::Cors::new()
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
)

This code is a so-called middleware, meaning that it will be run for every request received by the service, and so it is a piece of software that stays in the middle between the client and the server.

The wrap method is the one to use to add a piece of middleware. This word means that the following code must be around every handler, possibly filtering both requests and responses.

Such code creates an object of type Cors and specifies for it which HTTP methods will be accepted.

The rest of this web service should be clear to those who have learned what has already been described about the Actix web framework. It is a RESTful web service that accepts requests as URI paths and queries and returns responses as JSON bodies, and for which authentication is provided in any request by the basic authentication header.

The API has a new route for the GET method and the /authenticate path, which calls the authenticate handler, which is used to get a whole user object with the list of their privileges.

Now let's see how yclient is organized.

Theyclient app

This app starts from where the yauth app left off. The yauth apps contain its own in-memory database, while the app described here communicates with the person_db web service to access its database.

Here, we'll see only the new features, with respect to the yauth project.

The imported crates

The Cargo.toml file contains new lines:

failure = "0.1"
serde = "1"
serde_derive = "1"
url = "1"
base64 = "0.10"

For the preceding code, let's have a look at the following things:

  • The failure crate is used to encapsulate communication errors.
  • The serde and serde_derive crates are needed to transfer whole objects from server to client, using deserialization. In particular, the whole object of the types Person,User, andDbPrivilegeare transferred in server responses.
  • The url crate is used for encoding information in a URL. In a URL path or a URL query, you can easily put only identifiers or integer numbers, such as, say, /person/id/478 or/persons?ids=1,3,39, but more complex data, such as the name of a person, is not allowedas is. You cannot have a URL as /persons?partial_name=John Doe, because it contains whitespace. In general, you have to encode it in coding allowed in a URL, and that is provided by the call tourl::form_urlencoded::byte_serialize, which gets a slice of bytes and returns an iterator generating chars. If you callcollect::<String>() on this iterator, you get a string that can be safely put into a web URI.
  • The base64 crate is used to perform a similar encoding of binary data into textual data, but for the header or the body of an HTTP request. In particular, it is required to encode usernames and passwords in the basic authentication header.

The source files

The source file names are the same as the yauth project, except that the db_access.rs file has been renamed as common.rs. Actually, in this project, there is no code required to access the database, as access is now performed only by the service. The common module contains definitions of a constant, two structs, an enum, and a function needed by several components.

The changes to the models

The models of the components have the following changes.

All the db_connection fields have been removed, as the app now does not directly access the database. That has become the responsibility of the server.

The Boolean fetching field has been added. It is set to true when a request is sent to the server and reset to false when the response is received, or the request has failed. It is not really necessary in this app, but it may be useful when using a slower communication (with a remote server) or some more lengthy requests. It may be used to show to the user that a request is pending, and also to disable other requests in the meantime.

The fetch_service field has been added to provide the communication feature. The ft field has been added to contain a reference to the current FetchTask object during a request, or Nothing when no request has already been sent. This field is not actually used; this is just a trick to keep the current request alive, because otherwise after the request is sent and the update function returns, the local variables would be dropped.

The link field has been added for forwarding to the current model the callback that will be called when the response is received.

The console field has been added to provide a way to print to the console of the browser, for debugging purposes. In Yew, the print! and println! macros are ineffective, as there is no system console on which to print. But the web browser has a console, which is accessed using the console.log() JavaScript function call. This Yew service provides access to such a feature.

The username and password fields have been added to send authentication data with any requests.

But let's see the changes required to the code because of the need to communicate with the server.

A typical client/server request

For any user command that, in the yauth project, required access to the database, such access has been removed, and the following changes have been applied, instead.

Such a user command now sends a request to a web service, and then a response from that service must be handled. In our examples, the time between the user command and the reception of the response from the service is quite short – just a few milliseconds, for the following reasons:

  • Both client and server run in the same computer, and so the TCP/IP packets actually don't exit the computer.
  • The computer has nothing else to do.
  • The database is actually a very short memory vector, and so its operations are very fast.

Though, in a real system, much more time is spent processing a user command that causes communication. If everything is good, a command takes only half a second, but sometimes it may take several seconds. So, synchronous communication is not acceptable. Your app cannot just wait for a response from the server, because it would appear to be stuck.

So, the FetchService object of the Yew framework provides an asynchronous communication model.

The controller routine triggered by the user command prepares the request to be sent to the server, and also prepares a callback routine, to handle the response from the server, and then sends the request, and so the app is free to handle other messages.

When the response comes from the server, the response triggers a message that is handled by the controller. The handling of the message invokes the callback prepared in advance.

So, in addition to the messages signaling a user command, other messages have been added. Some of them report the reception of a response, that is, the successful completion of a request; and others report a failure of the request coming from the server, that is, the unsuccessful completion of a request. For example, in the PersonsListModel component, implemented in the persons_list.rs file, the following user actions required communication:

  • Pressing the Find button (triggering the FindPressed message)
  • Pressing the Filter button (triggering the FilterPressed message)
  • Pressing the Delete Selected Persons button (triggering theDeletePressedmessage)
  • Pressing one of the Edit buttons (triggering theEditPressedmessage)

For them, the following messages have been added:

  • ReadyFilteredPersons(Result<Vec<Person>, Error>): This is triggered by the FetchService instance when a list of filtered persons is received from the service. Such a list is contained in a Vec of Person. This may happen after processing theFilterPressed message.
  • ReadyDeletedPersons(Result<u32, Error>): This is triggered by the FetchService instance when the report that a command to delete some persons has been completed by the service. The number of deleted persons is contained in u32. This may happen after processing the DeletePressed message.
  • ReadyPersonToEdit(Result<Person, Error>): This is sent by FetchService when the requested Person object is received from the service, and so it can be edited (or simply displayed). This may happen after processing the FindPressed message or the EditPressed message.
  • Failure(String): This is sent by FetchService when any of the preceding requests have failed as the service returns a failure response.

For example, let's see the code that handles the EditPressed message. Its first part is as follows:

self.fetching = true;
self.console.log(&format!("EditPressed: {:?}.", id));
let callback =
self.link
.send_back(move |response: Response<Json<Result<Person, Error>>>| {
let (meta, Json(data)) = response.into_parts();
if meta.status.is_success() {
PersonsListMsg::ReadyPersonToEdit(data)
} else {
PersonsListMsg::Failure(
"No person found with the indicated id".to_string(),
)
}
});

Let's check the working of the code:

  • First, the fetching state is set to true, to take note that communication is underway.
  • Then a debug message is printed to the console of the browser.
  • Then, a callback is prepared to handle the response. To prepare such a callback, a moveclosure, that is, a closure that gets ownership of all the variables it uses, is passed to the send_back function of thelinkobject.
Remember that we come here when the user has pressed a button to edit a person specified by their ID; and so we need the whole of the person data to display it to the user.

The body of the callback is the code that we want to be executed after receiving a response from the server. Such a response, if successful, must contain all the data regarding the person we want to edit. So, this closure gets a Response object from the service. This type is actually parameterized by the possible contents of the response. In this project, we always expect a yew::format::Json payload and such a payload is a Result, which always has failure::Error as its error type. Though, the success type varies depending on the request type. In this particular request, we expect a Person object as a successful result.

The body of the closure calls the into_parts method on the response to destructure the response into the metadata and the data. The metadata is HTTP-specific information, while the data is the JSON payload.

Using the metadata, it is possible to check whether the response was successful (meta.status.is_success()). In such a case, the Yew message ReadyPersonToEdit(data) is triggered; such a message will handle the response payload. In the event of an error, a Yew message of Failure is triggered; such a message will display the specified error message.

You could ask: "Why does the callback forward the payload to the Yew framework, specifying another message, instead of doing anything that should be done upon receipt of the response?"

The reason is that the callback, to be executed out of context by the framework, must be the owner of any variable it accesses after its creation, that is, when the request is sent, up to the time of its destruction (when the response is received). So, it cannot use the model or any other external variable. You cannot even print on the console or open an alert box inside such a callback. So you need to asynchronously forward the response to a message handler, which will be able to access the model.

The remaining part of the handler of theEditPressedmessage is this:

let mut request = Request::get(format!("{}person/id/{}", BACKEND_SITE, id))
.body(Nothing)
.unwrap();

add_auth(&self.username, &self.password, &mut request);
self.ft = Some(self.fetch_service.fetch(request, callback));

First, a web request is prepared, using the get method, which uses the GET HTTP method, and optionally specifying a body, which in this case is empty (Nothing).

Such a request is enriched with authentication information by a call of the add_auth common function, and finally, the fetch method of the FetchService object is invoked. This method uses the request and the callback to begin the communication with the server. It immediately returns a handle, stored in the ft field of the model.

Then the control returns to Yew, which can process other messages, until a response comes from the server. Such a response will be forwarded to the callback defined before.

Now, let's see the handler of the ReadyPersonToEdit(person) message, forwarded when a person structure is received from the server as a response to the request of editing a person by their id. Its code is as follows:

self.fetching = false;
let person = person.unwrap_or(Person {
id: 0,
name: "".to_string(),
});
if let Some(ref go_to_page) = self.go_to_one_person_page {
self.console
.log(&format!("ReadyPersonToEdit: {:?}.", person));
go_to_page.emit(Some(person.clone()));
}

First, the fetching state is set tofalse, to take note that the current communication is ended.

Then, if the received person was None, such a value is replaced by a person having zero as id and an empty string as a name. Of course, it is an invalid person.

Then, a reference to the go_to_one_person_page field of the model is taken. This field can be None (in fact, only at the initialization stage), so, if it is not defined, nothing is done. This field is a Yew callback to jump to another page.

At last, a debug message is printed, and the callback is invoked using the emit method. This call receives a copy of the person to display on that page.

Now, let's see the handler of the Failure(msg) message, forwarded when an error is received from the server. This handler is shared by other requests, as it has the same behavior. Its code is as follows:

self.fetching = false;
self.console.log(&format!("Failure: {:?}.", msg));
self.dialog.alert(&msg);
return false;

Again, the fetching state is set to false since the communication is ended.

A debug message is printed, and a message box is opened to show the user the error message. As long as such a message box is opened, the component is frozen, as no other message can be processed.

At last, the controller returns false to signal that no view needs to be refreshed. Notice that the default return value is true as, usually, the controller changes the model, and so the view must be refreshed as a consequence of that.

Summary

We have seen how a complete frontend web app can be built using Rust, by using the cargo-web command, the Wasm code generator, and the Yew framework. Such apps are modular and well structured, as they use the Elm Architecture, which is a variant of the MVC architectural pattern.

We created six apps, and we saw how they worked—incr, adder, login, yauth, persons_db, and yclient.

In particular, you learned how to build and run a Wasm project. We looked at the MVC architectural pattern for building interactive apps. We covered how the Yew framework supports the creation of apps implementing an MVC pattern, specifically according to the Elm Architecture. We also saw how to structure an app in several components and how to keep a common header and footer, while the body of the app changes from page to page. And at the end, we learned how to use Yew to communicate with a backend app, possibly running on a different computer, packaging data in JSON format.

In the next chapter, we will see how to build a web game using Wasm and the Quicksilver framework.

Questions

  1. What is WebAssembly, and what are its advantages?
  2. What is the MVC pattern?
  3. What are messages in the Elm Architecture?
  4. What are components in the Yew framework?
  5. What are properties in the Yew framework?
  6. How can you build a web app with a fixed header and footer and change the inner section using the Yew framework?
  7. What are callbacks in the Yew framework?
  8. How can you pass a shared object, such as a database connection, between Yew components?
  9. Why you must keep in the model a field having type FetchTask, when you communicate with a server, even if you don't need to use it?
  10. How can you open JavaScript-style alert boxes and confirm boxes using the Yew framework?

Further reading

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset