© Yiyi Sun 2019
Yiyi SunPractical Application Development with AppRunhttps://doi.org/10.1007/978-1-4842-4069-4_3

3. Model the State

Yiyi Sun1 
(1)
Thornhill, ON, Canada
 

This chapter is a deep dive into the state concept of AppRun. The state is one of three main parts of the AppRun architecture. It plays an important role in the AppRun event lifecycle. It is equivalent to the mode of the Elm architecture. Elm defines the model as the application state. If fact, model and state are two names for the same thing. They are interchangeable in the AppRun architecture. Most of the time, we use the term state in AppRun.

In this chapter, you will learn about the concept of state, how to time-travel through the application state history to develop the undo and redo feature, how to save states locally and to the cloud, and how to sync states across multiple devices.

State Concept

The state is the application state at any given time of your application. As highlighted in Figure 3-1, the state is the data flow between the update and the view. It acts as the data transfer object (DTO) in a traditional multilayered application architecture, where the DTO is an object that carries data between the logical and physical layers.
../images/467411_1_En_3_Chapter/467411_1_En_3_Fig1_HTML.png
Figure 3-1

State flow in the AppRun applications

When AppRun applications are starting, the initial state is used to render the web page.

    Initial state => View => (HTML/Virtual DOM) => render DOM    [1]

When AppRun applications are running, AppRun manages the state flow through the event handlers and then through the view function and renders the web page during the event lifecycle.

Web events => AppRun events => (current state) => Update => (new state) => View => (HTML/Virtual DOM) => render DOM => (new state) => rendered    [2]

To demonstrate the state concept, we will develop the counter application introduced in Chapter 1 using the AppRun CLI and the AppRun development environment introduced in Chapter 2, which allows us to use the ECMAScript 2015 module format and JSX (Listing 3-1).
1.   import app from 'apprun';
2.   const state = 0;
3.   const view = (state) => <div>
4.       <h1>{state}</h1>
5.       <button onclick={()=>app.run('-1')}>-1</button>
6.       <button onclick={()=>app.run('+1')}>+1</button>
7.   </div>;
8.   const update = {
9.       '+1': (state) => state + 1,
10.      '-1': (state) => state - 1
11.  };
12.  app.on('debug', p => console.log(p));
13.  app.start('my-app', state, view, update);  
Listing 3-1

Source Code of the Counter Application (3-1.tsx)

Initial State

The counter application starts with an initial state, the number 0 (line 2). It is used to start the application in the app.start function (line 13). The state uses the keyword const instead of let. It won’t change after the application starts. The initial state is immutable. Sometimes we can pass the initial state into the app.start function without defining a state variable.
app.start('my-app', 0, view, update);

State History

AppRun has built-in state history and the state history pointer. In Chapter 1’s example, we demonstrated the state history in the counter application. The back button (<<) steps back in the state history, or undoes the counter change. The forward button (>>) steps forward in the state history, or redoes the counter change. In Listing 3-2, we make a JSX version (Listing 3-2) and analyze the state history pointer movement.
1.   import app from 'apprun';
2.   const state = 0;
3.   const view = (state) => {
4.   console.log(state)
5.   return <div>
6.       <button onclick={() => app.run("history-prev")}> << </button>
7.       <button onclick={() => app.run("history-next")}> >> </button>
8.       <h1>{state}</h1>
9.       <button onclick={() => app.run('-1')}>-1</button>
10.      <button onclick={() => app.run('+1')}>+1</button>
11.  </div>;
12.  }
13.  const update = {
14.      '+1': (state) => state + 1,
15.      '-1': (state) => state - 1
16.  };
17.  app.start('my-app', state, view, update, {history: true});
Listing 3-2

Source Code of the Counter Application with History

We set the history option to true in the app.start function call (line 17) to enable the state history. The back (<<) button publishes the history-prev event to let AppRun set the current state to the state before the state history pointer (line 6). The forward (>>) button publishes the history-next event to let AppRun set the current state to the state after the state history pointer.

Let’s visualize the state history and the state history pointer. First, we increase the number to 3; next, we click the back (<<) button three times; finally, we click the forward (>>) button three times. Table 3-1 shows the state history changes.
Table 3-1

State History

 

State to View

State History and Pointer

The initial state.

0

0 <=

Increase the counter.

1

0

1 <=

Increase the counter.

2

0

1

2 <=

Increase the counter.

3

0

1

2

3 <=

Click the back (<<) button

(take the state before the point).

2

0

1

2 <=

3

Click the back (<<) button

(take the state before the point).

1

0

1 <=

2

3

Click the back (<<) button

(take the state before the point).

0

0 <=

1

2

3

Click the forward (>>) button

(take the state after the point).

1

0

1 <=

2

3

Click the forward (>>) button

(take the state after the point).

2

0

1

2 <=

3

Click the forward (>>) button

(take the state after the point).

3

0

1

2

3 <=

Although it is easy to enable the AppRun state history, the caveat is that it requires the state to be immutable. Because in the AppRun state history it stores the references to the states, if we have modified the state directly, each state stored in the state history refers the same state, which is always the value of last change. The time-travel back and forward will not work. The fundamental concept of using the state history is to make the state immutable.

Immutable State

Primitive data types in JavaScript, such as number, string, boolean, null, and undefined, are immutable already. The counter application has the state of type number, which is immutable out of the box. We can quickly enable the state history and the time-travel feature.

Nonprimitive data types such as array and object are mutable. When the state of an application is an array or an object, we need to leave the current state alone and always create a new state based on the current state.

Immutable Array

To demonstrate how to make the immutable state of the array, we will make a multiple-counter application. The multicounter application is a list of counters. It adds two buttons to the original back and forward buttons: one adds a new counter, and the other removes the last counter (see Figure 3-2). Each counter has three buttons: a button to increase the counter, a button to decrease the counter, and a new button to remove the counter from the counter list.
../images/467411_1_En_3_Chapter/467411_1_En_3_Fig2_HTML.jpg
Figure 3-2

Multicounter application

The three application building blocks of the multicounter application are as follows:
  • The state of the multicounter application is an array of numbers.

  • The view function displays the buttons and the counter list.

  • The update object has three event handlers for adding a counter, removing the counter, and updating the counters.

Let’s dive deep into the state of the multicounter application. The state is an array of numbers. Each counter is a number within the array. The state is empty initially. We can add new counters, and we can remove counters. When a counter is added, a new number is added to the array. When a counter is removed, the correspondent number is removed from the array.

There are three events: add-counter, remove-counter, and update-counter. The add-counter and remove-counter events add and remove elements to and from the array, respectively. The update-counter event increases and decreases the element based on its index inside the array by the delta value.

Usually, we use the array.push function to add an element to the array and use the array.splice function to remove an element from the array. To update the counter in the array, we also usually just retrieve the element by index and update it directly. However, these functions mutate the array. In other words, they change the content in the array. We need different approaches.

Instead of using the array.push function to add an element to the array, we can use the spread operator defined in ECMAScript 2015. We create a new array, spread in old array elements, and add 0 to the end.
  (state) => [...state, 0],
Instead of using the array.splice function to remove an element from the array by its index, we can use the spread operator twice. We create a new array and spread in elements before and after the index.
  (state, idx) => [
     ...state.slice(0, idx),
     ...state.slice(idx + 1)
   ],
Instead of updating the element in the array directly, we can follow the same idea of composing the new array.
  (state, idx, delta) => [
    ...state.slice(0, idx),
    state[idx] + delta,
    ...state.slice(idx + 1)
  ]
Based on previous immutable array operations, the technique to make immutable the state of an array is to break down the existing array and recompose it. The multicounter application implements the technique (Listing 3-3).
1.   import app from 'apprun';
2.   const state = [];
3.   const view = (state) => <div>
4.       <div>
5.           <button onclick={() => app.run("history-prev")}> << </button>
6.           <button onclick={() => app.run("history-next")}> >> </button>
7.           <button onclick={() => app.run("add-counter")}>add counter</button>
8.           <button onclick={() => app.run("remove-counter")} disabled={state.length <= 0}>remove counter</button>
9.       </div>
10.      { state.counters.map((num, idx) => <div>
11.          <h1>{num}</h1>
12.          <button onclick={() => app.run("update-counter", idx, -1)}>-1</button>
13.          <button onclick={() => app.run("update-counter", idx, 1)}>+1</button>
14.          <button onclick={() => app.run("remove-counter", idx)}>x</button>
15.      </div>
16.      )}
17.      </div>);
18.  };
19.  const update = {
20.      'add-counter': (state) => [...state, 0],
21.      'remove-counter': (state, idx = state.length - 1) => [
22.          ...state.slice(0, idx),
23.          ...state.slice(idx + 1)
24.      ],
25.      'update-counter': (state, idx, delta) => [
26.          ...state.slice(0, idx),
27.          state[idx] + delta,
28.          ...state.slice(idx + 1)
29.      ]
30.  };
31.  app.start('my-app', state, view, update, {history: true});
Listing 3-3

Source Code of the Multicounter Application

When running Listing 3-3, you can see that the back (<<) and forward (>>) buttons travel through the state change history just like the single-counter application (see the states printed in the console pane in Figure 3-2).

Immutable Object

The same technique of making immutable the array state applies to immutable object state. To demonstrate the immutable state of the object, we will build a to-do application (see Figure 3-3), similar to the ToDoMVC applications. The ToDoMVC website ( http://todomvc.com ) has a list of 60+ implementations of to-do applications for studying, comparing, and evaluating features, project structure, and application architecture using different frameworks and libraries. Modeled after the ToDoMVC projects, the functional specifications of our to-do application are as follows:
  • Allow the user to add a new to-do item to the to-do list

  • Allow the user to toggle the to-do item from active to complete

  • Allow the user to delete the to-do items

  • Allow the user to view the to-do items by category: all, active, and complete

  • Allow the user to see the total number of active to-do items

  • Allow undo and redo

  • Save the to-do list locally on the computer

  • Save the to-do list across multiple devices

../images/467411_1_En_3_Chapter/467411_1_En_3_Fig3_HTML.jpg
Figure 3-3

To-do application

We will develop the to-do application’s undo and redo features in this section. The last two requirements will be developed in the next two sections.

The state of the to-do application should cover all the requirements of the specification. We can build it piece by piece starting with the to-do item: TodoItem . A to-do item has a title and done flag. The done can be true or false to indicate whether the to-do item has been completed. A state is an object that has an array of to-do items and a filter that has three options: All, Active, and Complete (Listing 3-4).
1.   type TodoItem = {
2.       title: string;
3.       done: boolean;
4.   }
5.   type State = {
6.       filter: 'All' | 'Active' | 'Complete',
7.       list: Array<TodoItem>
8.   };
9.   const state: State = {
10.      filter: 'All',
11.      list: []
12.  };
Listing 3-4

The State of the To-Do Application

There are four event handlers (add-item, delete-item, toggle-item, and filter-item) for manipulating to-do items as well as an event handler for detecting the Return key press for adding new to-do items (Listing 3-5).
1.   const update = {
2.       'add-item': (state) => { },
3.       'delete-item': (state, idx) => { },
4.       'toggle-item': (state, idx) => { },
5.       'filter-item': (state, e) => { },
6.       'keyup': (state, e) => { }
7.   };
Listing 3-5

The Event Handlers of the To-Do Application

AppRun passes the current state into the event handlers. When the function parameter is an array or object, it is passed to the function as a reference. The state object can be changed inside the event handler. To create a new state based on the current state, we use the spread operator to break down the current state object properties, insert them into a new object, and then overwrite the properties with the new value according to the events. For example, the event handler of toggle-item overwrites the filter property of the new state object.
'filter-item': (state, e) => ({ ...state, filter: e.target.textContent })
The method applies to all simple property types. But the to-do item list in the state is an array. Adding, deleting, and changing the items inside the list should follow the technique of an immutable array (Listing 3-6).
1.   'add-item': (state, title) => ({
2.       ...state,
3.       list: [...state.list, { title, done: false }]
4.   }),
5.   'delete-item': (state, idx) => ({
6.       ...state,
7.       list: [
8.           ...state.list.slice(0, idx),
9.           ...state.list.slice(idx + 1)
10.          ]
11.      }),
12.      'toggle-item': (state, idx) => ({
13.          ...state,
14.          list: [
15.              ...state.list.slice(0, idx),
16.              { ...state.list[idx], done: !state.list[idx].done },
17.              ...state.list.slice(idx + 1)
18.          ]
19.      }),
Listing 3-6

Immutable State Update in the To-Do Application

The code snippets to add, delete, and change the list items within a state object (Listing 3-6) provide a reusable pattern that you can use in your applications. The complete source code of the preliminary to-do application without visual styling has 78 lines of code including static types (Listing 3-7).
1.   import app from 'apprun';
2.   type TodoItem = {
3.       title: string;
4.       done: boolean;
5.   }
6.   type State = {
7.       filter: 'All' | 'Active' | 'Complete';
8.       list: Array<TodoItem>;
9.   };
10.  const state: State = {
11.      filter: 'All',
12.      list: []
13.  };
14.  const view = (state: State) => {
15.      const countAll = state.list.length;
16.      const countActive = state.list.filter(todo => !todo.done).length || 0;
17.      const countComplete = state.list.filter(todo => todo.done).length || 0;
18.      return <div>
19.          <button onclick={() => app.run("history-prev")}> << </button>
20.          <button onclick={() => app.run("history-next")}> >> </button>
21.          <p><input onkeyup={e => app.run('keyup', e)} /></p>
22.          <ul> {
23.                  state.list
24.                      .map((todo, idx) => ({ ...todo, idx }))
25.                      .filter(todo => state.filter === 'All' ||
26.                               (state.filter === 'Active' && !todo.done) ||
27.                               (state.filter === 'Complete' && todo.done))
28.                      .map((todo) => <li>
29.                          <input type="checkbox" onclick={() => app.run('toggle-item', todo.idx)} checked={todo.done} />
30.                          <span>{todo.title} {' '} (<a href='#' onclick={() => app.run('delete-item', todo.idx)}>␡</a>)</span>
31.                      </li>)
32.          }</ul>
33.          <div>
34.          <a href='#' onclick={e => app.run('filter-item', e)}>All</a> {` (${countAll}) | `}
35.          <a href='#' onclick={e => app.run('filter-item', e)}>Active</a> {`(${countActive}) | `}
36.          <a href='#' onclick={e => app.run('filter-item', e)}>Complete</a> {`(${countComplete})`}
37.      </div>
38.  </div>
39.  };
40.  const update = {
41.      'add-item': (state, title) => ({
42.          ...state,
43.          list: [...state.list, { title, done: false }]
44.      }),
45.      'delete-item': (state, idx) => ({
46.          ...state,
47.          list: [
48.              ...state.list.slice(0, idx),
49.              ...state.list.slice(idx + 1)
50.          ]
51.      }),
52.      'toggle-item': (state, idx) => ({
53.          ...state,
54.          list: [
55.              ...state.list.slice(0, idx),
56.              { ...state.list[idx], done: !state.list[idx].done },
57.              ...state.list.slice(idx + 1)
58.          ]
59.      }),
60.      'filter-item': (state, e) => ({ ...state, filter: e.target.textContent }),
61.      'keyup': (state, e) => {
62.          if (e.keyCode === 13 && e.target.value.trim()) {
63.              app.run('add-item', e.target.value);
64.              e.target.value = ";
65.          }
66.      }
67.  };
68.  app.start('my-app', state, view, update, { history: true });
Listing 3-7

Complete Source Code of the To-Do Application

We can conclude that the method to make the state immutable is to create a new state that replaces the current state. The AppRun architecture is designed to support the immutable state. If we implement the immutable state, AppRun provides the time-travel through the state history, which is useful for developing the undo and redo features. On the other hand, although making the state immutable is always a good practice, it does require extra attention and coding effort in JavaScript.

Note

The immutable state is not a mandatory requirement of AppRun. If you do not need the undo and redo feature in your application, you can mutate the current state. AppRun is flexible.

Persistent State

The state is like the soul of an AppRun application. We can save the state after the state change and load the state to resume the application. We can also share the state across browsers, platforms, and devices to run the applications simultaneously in different browsers and apps on various devices. We will demonstrate the local state and cloud state in the next two sections.

Local State

We will continue developing the to-do application by adding a new feature to store the to-do list on the local computer. The application saves the to-do list to the browser’s local storage to preserve the state. When the application starts, it loads and renders the state automatically (see Figure 3-4).
../images/467411_1_En_3_Chapter/467411_1_En_3_Fig4_HTML.jpg
Figure 3-4

Local storage of the to-do application

Local storage is a browser feature to allow web applications to store data locally within the user’s browser. It is secure, it is high performant, and it allows a large amount of data to be stored (at least 5MB). All pages, from the same origin (per domain and protocol), can save and load the data to and from the local storage. The Chrome browser DevTool displays the local storage content on the Application tab (see Figure 3-4).

Loading data and saving data are synchronous operations that can easily apply to AppRun applications (Listing 3-8).
1.   import app from 'apprun';
2.   const state: State = {};
3.   const view = (state: State) => {};
4.   const update = {};
5.   const STORAGE_KEY = 'to-do-list';
6.   const rendered = state => localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
7.   const stored = localStorage.getItem(STORAGE_KEY)
8.   app.start('my-app', stored ? JSON.parse(stored) : state, view, update, { rendered });
Listing 3-8

Enable Local Storage in AppRun Applications

Loading the state from the local storage, we need to pay attention to a couple of details. The data stored in the local storage is always the string type. Therefore, it requires us to serialize and deserialize the state when saving to and loading from the local storage (line 7). When the first-time application runs, the state does not exist in the local storage. We fall back to using the default initial state (line 8).

To save the state to the local storage, we use the rendered callback function in the AppRun event lifecycle (see Figure 3-1). It is the last step before the event cycle ends. AppRun invokes the rendered callback function with the state parameter after it renders the HTML to the web page.

We create the rendered function for saving the state to local storage (line 6) and set the rendered callback function in the options parameter when we start the application (line 8).

By simply adding the logic of loading and saving the state (Listing 3-8, lines 6–8) to the to-do application, we have enabled local persistent storage to the application. Listing 3-8 is a reusable pattern that you can follow when developing other AppRun applications .

Cloud State

Although the local storage allows the data access across many browsers of the same type, by default the data is not accessible between different browser types. For example, Firefox browsers cannot access the local storage of the Chrome browsers. However, AppRun applications can run on multiple browsers and even various platforms and devices. We will finish this chapter by making the to-do application share the state across browsers, platforms, and devices.

We will save the state to Google Cloud Firestore and leverage Cloud Firestore to sync the data across multiple devices. Cloud Firestore is a cloud NoSQL database for developing mobile, web, and server applications from Firebase and Google Cloud Platform. It keeps your data in sync across devices in real time and offers offline support for mobile and web applications. To add Google Cloud Firestore to the AppRun application, follow the steps:
  1. 1.

    Open the Firebase Console ( https://console.firebase.google.com /), add a new project, and then enter your project name. If you have an existing Firebase project that you’d like to use, select that project from the console.

     
  2. 2.

    In the Database section, click the Get Started button for Cloud Firestore. Select the Test mode as the starting mode (remember to switch to Locked mode for production use later) and then click the Enable button.

     
  3. 3.

    From the project overview page, add Firebase to your web project.

     
  4. 4.

    Copy the configuration (Listing 3-9) and paste it into the store module (Listing 3-10).

     
  5. 5.

    Install Firebase in your AppRun project: npm i firebase.

     
1.   var  config = {
2.       apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
3.       authDomain: "apprun-demo.firebaseapp.com",
4.       databaseURL: "https://apprun-demo.firebaseio.com",
5.       projectId: "apprun-demo",
6.       storageBucket: "apprun-demo.appspot.com",
7.       messagingSenderId: "--------------"
8.   };
Listing 3-9

Firebase Web Project Configuration

We will use the configuration to create a store module that handles the events for saving and loading data to and from Firestore (Listing 3-10).
1.   import app from 'apprun';
2.   import * as firebase from 'firebase';
3.   import 'firebase/firestore';
4.   const STORAGE_KEY = 'to-do-list';
5.   const config = {
6.       apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",  ,
7.       authDomain: "apprun-demo.firebaseapp.com",
8.       databaseURL: "https://apprun-demo.firebaseio.com",
9.       projectId: "apprun-demo",
10.      storageBucket: "apprun-demo.appspot.com",
11.      messagingSenderId: "-------------"
12.  };
13.  firebase.initializeApp(config);
14.  const db = firebase.firestore();
15.  const ref = db.collection(STORAGE_KEY).doc("state")
16.  app.on('save-state', state => ref.set(state));
17.  ref.onSnapshot(doc => {
18.      if (doc.exists) app.run('new-state', doc.data())
19.  });
Listing 3-10

Module for Saving and Load Data to and from Firestore

Firestore access uses the event publication and subscription pattern just like AppRun. It is a natural fit for the AppRun applications. The store module (Listing 3-10) uses an AppRun event subscription for saving data to Firestore. Other AppRun application modules have no dependencies to the Firebase and Firestore library.

The store module subscribes to the save-state event (line 16). When the application needs to save the state, we can publish the save-state event with the state as event parameters to let the store module save that state to Firestore.

The store module also publishes the new-state event. When Firestore data is available in the Firestore onSnapshot event, it converts the Firestore onSnapshot event to the AppRun new-state event (lines 17–19).

The new-state and save-state events connect the to-do application with Firestore (Listing 3-11). The state and the view function are omitted to let you focus on the most relevant code that saves and loads the state in Listing 3-11.
1.   import app from 'apprun';
2.   import './store';
3.   const state = {...};
4.   const view = (state) => {... };
5.   const update = {
6.       'new-state': (_, state) => state,
7.       'add-item': (state, title) => app.run('save-state', {
8.           ...state,
9.        }),
10.      'delete-item': (state, idx) => app.run('save-state', {
11.          ...state,
12.       }),
13.      'toggle-item': (state, idx) => app.run('save-state', {
14.          ...state,
15.      })
16.  };
17.  app.start('my-app', state,  view, update, { history: true });
Listing 3-11

Saving the State to the Cloud

When the application connects to Firestore the first time, Firestore publishes the onSnapshot event . The store module publishes the AppRun new-state event. The new-state event handler returns the state to AppRun to render the web page (line 7).

When the users add, delete, and toggle to-do items, the corresponding event handlers of add-item, delete-item, and toggle-item publish the save-state event to let the store module save the state to Firestore.

When Firestore saves the state successfully, it publishes the onSnapshot event again. The onSnapshot event is converted to the new-state event again. AppRun renders the new state on the web page (see Figure 3-5).
../images/467411_1_En_3_Chapter/467411_1_En_3_Fig5_HTML.png
Figure 3-5

Firestore events and AppRun events

The event handlers of the add-item, delete-item, and toggle-item events call the app.run function only. They do not return any state (Lines 7-16). When the event handler returns a new state of the null or undefined object, the AppRun stops the AppRun event lifecycle at checkpoint 1 (see Figure 3-1). AppRun does not invoke the view function anymore. The event lifecycle stops. Nothing changes on the screen until the new-state event comes later.

We can open the to-do application in different browsers. Figure 3-6 shows the application, from left, in Chrome, Firefox, and Edge. The to-do list is automatically shared between them. When adding, toggling, and deleting the to-do items in one browser, the new to-do list is automatically displayed in the other browsers without a need to refresh the other browsers.
../images/467411_1_En_3_Chapter/467411_1_En_3_Fig6_HTML.jpg
Figure 3-6

Running the to-do application in multiple browsers

Source Code of Examples

You can get the source code in this chapter by cloning the GitHub project from https://github.com/yysun/apprun-apress-book . You can run the six examples in this chapter using the npm scripts in Table 3-2.
Table 3-2

npm Scripts of This Chapter

Example

Script

The counter application (Listing 3-1)

npm run jsx-counter

The counter with history (Listing 3-2)

npm run jsx-counter-history

The multiple counter application (Listing 3-3)

npm run counters

The to-do application (Listing 3-8)

npm run to-do

The local to-do application (Listing 3-9)

npm run to-do-local

The cloud to-do application (Listing 3-11)

npm run to-do-cloud

Summary

In AppRun applications, the state is the DTO between the event handlers and the view function. AppRun manages the state flow. It takes an initial state and keeps tracks of the state and manages the state history. You can travel through the state history if you keep the state immutable by always creating a new state to replace the current state.

The state is the soul of the AppRun application. Persisting the state in the local storage allows the user to exit the applications without losing data. Sharing the state into the cloud makes the applications run across browsers, platforms, and devices.

When the applications become complicated, the states of the applications become complicated. They can have the data fields and visual flag fields. The data fields are the dynamic content to render the web pages. The visual flag fields are to control the visual presentation of web page elements such as the visibility, color, open and collapse status, class, and styles of the web page elements. In the next chapter, we will introduce the various strategies, patterns, and techniques of rendering the state to the web pages in the view function.

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

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