©  Caio Ribeiro Pereira 2016

Caio Ribeiro Pereira, Building APIs with Node.js , 10.1007/978-1-4842-2442-7_13

13. Building the Client-Side App: Part 2

Caio Ribeiro Pereira

(1)São Vicente - SP, São Paulo, Brazil

Continuing with the client application construction, to this point we have an application with the main layout that connects with the API and allows us to authenticate a user to access the system. In this chapter, we are going to build the main features for users to be able to manage their tasks.

Views and Components for Task’s CRUD

The tasks template construction will be quite complex, but in the end, it will be great. This template must list all of the user’s tasks, so this function will receive as an argument a tasks list. Using the tasks.map() function, it will generate a template’s array of the tasks.

At the end of this new array generation, the function .join("") will be executed and will concatenate all items returning a single template string of tasks. To make this manipulation and generation of the tasks templates easier, this logic is encapsulated via the renderTasks(tasks) function. Otherwise, it displays a message about an empty task list.

To understand better this implementation, create the file src/templates/tasks.js as shown here.

 1   const renderTasks = tasks => {
 2     return tasks.map(task => {
 3       let done = task.done ? "ios-checkmark" : "ios-circle-outline";
 4       return `<li class="item item-icon-left item-button-right">
 5         <i class="icon ion-${done}" data-done
 6           data-task-done="${task.done ? 'done' : ''}"
 7           data-task-id="${task.id}"></i>
 8         ${task.title}
 9         <button data-remove data-task-id="${task.id}"
10           class="button button-assertive">
11           <i class="ion-trash-a"></i>
12         </button>
13       </li>`;
14     }).join("");
15   };
16   exports.render = tasks => {
17     if (tasks && tasks.length) {
18       return `<ul class="list">${renderTasks(tasks)}</ul>`;
19     }
20     return `<h4 class="text-center">The task list is empty</h4>`;
21   };

Using the attributes data-task-done, data-task-id, data-done, and data-remove, we work on how to create components for tasks manipulation. These attributes will be used to trigger some events to allow deleting a task (via method taskRemoveClick())or setting which task will be performed (via method taskDoneCheckbox()). These business rules will be written in the src/components/tasks.js file.

 1   import NTask from "../ntask.js";
 2   import Template from "../templates/tasks.js";
 3  
 4   class Tasks extends NTask {
 5     constructor(body) {
 6       super();
 7       this.body = body;
 8     }
 9     render() {
10       this.renderTaskList();
11     }
12     addEventListener() {
13       this.taskDoneCheckbox();
14       this.taskRemoveClick();
15     }
16     renderTaskList() {
17       const opts = {
18         method: "GET",
19         url: `${this.URL}/tasks`,
20         json: true,
21         headers: {
22           authorization: localStorage.getItem("token")
23         }
24       };
25       this.request(opts, (err, resp, data) => {
26         if (err) {
27           this.emit("error", err);
28         } else {
29           this.body.innerHTML = Template.render(data);
30           this.addEventListener();
31         }
32       });
33     }
34     taskDoneCheckbox() {
35     const dones = this.body.querySelectorAll("[data-done]");
36     for(let i = 0, max = dones.length; i < max; i++) {
37         dones[i].addEventListener("click", (e) => {
38           e.preventDefault();
39           const id = e.target.getAttribute("data-task-id");
40           const done = e.target.getAttribute("data-task-done");
41           const opts = {
42             method: "PUT",
43             url: `${this.URL}/tasks/${id}`,
44             headers: {
45               authorization : localStorage.getItem("token"),
46               "Content-Type": "application/json"
47             },
48             body: JSON.stringify({done: !done})
49           };
50           this.request(opts, (err, resp, data) => {
51             if (err || resp.status === 412) {
52               this.emit("update-error", err);
53             } else {
54               this.emit("update");
55             }
56           });
57         });
58       }
59     }
60     taskRemoveClick() {
61       const removes = this.body.querySelectorAll("[data-remove]");
62       for(let i = 0, max = removes.length; i < max; i++) {
63         removes[i].addEventListener("click", (e) => {
64           e.preventDefault();
65           if (confirm("Do you really wanna delete this task?")) {
66             const id = e.target.getAttribute("data-task-id");
67             const opts = {
68               method: "DELETE",
69               url: `${this.URL}/tasks/${id}`,
70               headers: {
71                 authorization: localStorage.getItem("token")
72               }
73             };
74             this.request(opts, (err, resp, data) => {
75               if (err || resp.status === 412) {
76                 this.emit("remove-error", err);
77               } else {
78                 this.emit("remove");
79               }
80             });
81           }
82         });
83       }
84     }
85   }
86
87   module.exports = Tasks ;

Now that we have the component responsible for listing, updating, and deleting tasks, let’s implement the template and component responsible for adding a new task. This will be easier, because it will be a template with a simple form to register new tasks, and in the end, it redirects to the task list. To do it, create the file src/templates/taskForm.js.

 1   exports.render = () => {
 2     return `<form>
 3       <div class="list">
 4         <label class="item item-input item-stacked-label">
 5           <span class="input-label">Task</span>
 6           <input type="text" data-task>
 7         </label>
 8       </div>
 9       <div class="padding">
10         <button class="button button-positive button-block">
11           <i class="ion-compose"></i> Add
12         </button>
13       </div>
14     </form>`;
15   };

Then, create its respective component, which will have only the form submission event from the encapsulated function formSubmit(). Create the file src/components/taskForm.js.

 1   import NTask from "../ntask.js";
 2   import Template from "../templates/taskForm.js";
 3  
 4   class TaskForm extends NTask {
 5     constructor(body) {
 6       super();
 7       this.body = body;
 8     }
 9     render() {
10       this.body.innerHTML = Template.render();
11       this.body.querySelector("[data-task]").focus();
12       this.addEventListener();
13     }
14     addEventListener() {
15       this.formSubmit ();
16     }
17     formSubmit() {
18       const form = this.body.querySelector("form");
19       form.addEventListener("submit", (e) => {
20         e.preventDefault();
21         const task = e.target.querySelector("[data-task]");
22         const opts = {
23           method: "POST",
24           url: `${this.URL}/tasks`,
25           json: true,
26           headers: {
27             authorization: localStorage.getItem("token")
28           },
29           body: {
30             title: task.value
31           }
32         };
33         this.request(opts, (err, resp, data) => {
34           if (err || resp.status === 412) {
35             this.emit("error");
36           } else {
37             this.emit("submit");
38           }
39         });
40       });
41     }
42   }
43
44   module.exports = TaskForm;

Views and Components for Logged Users

To finish the screen creation of our application, we build the last screen, which will display the logged user’s data and a button to be allow the user to cancel his or her account. This screen will have a component that is very easy to implement as well, because this will only treat the event of the account’s cancellation button. Create the src/templates/user.js file.

 1   exports.render = user => {
 2     return `<div class="list">
 3       <label class="item item-input item-stacked-label">
 4         <span class="input-label">Name</span>
 5         <small class="dark">${user.name}</small>
 6       </label>
 7       <label class="item item-input item-stacked-label">
 8         <span class="input-label">Email</span>
 9         <small class="dark">${user.email}</small>
10       </label>
11     </div>
12     <div class="padding">
13       <button data-remove-account
14         class="button button-assertive button-block">
15         <i class="ion-trash-a"></i> Cancel account
16       </button>
17     </div>`;
18   };

Now that we have the user’s screen template, let’s create its respective component in the src/components/user.js file, following this code.

 1   import NTask from "../ntask.js";
 2   import Template from "../templates/user.js";
 3  
 4   class User extends NTask {
 5     constructor(body) {
 6       super();
 7       this.body = body;
 8     }
 9     render() {
10       this.renderUserData();
11     }
12     addEventListener() {
13       this.userCancelClick();
14     }
15     renderUserData() {
16       const opts = {
17         method: "GET",
18         url: `${this.URL}/user`,
19         json: true,
20         headers: {
21           authorization: localStorage.getItem("token")
22         }
23       };
24       this.request(opts, (err, resp, data) => {
25         if (err || resp.status === 412) {
26           this.emit("error", err);
27         } else {
28           this.body.innerHTML = Template.render(data);
29           this.addEventListener ();
30         }
31       });
32     }
33     userCancelClick() {
34       const button = this.body.querySelector("[data-remove-account]");
35       button.addEventListener("click", (e) => {
36         e.preventDefault();
37         if (confirm("This will cancel your account, are you sure?")) {
38           const opts = {
39             method: "DELETE",
40             url: `${this.URL}/user`,
41             headers: {
42               authorization: localStorage.getItem("token")
43             }
44           };
45           this.request(opts, (err, resp, data) => {
46             if (err || resp.status === 412) {
47               this.emit("remove-error", err);
48             } else {
49               this.emit("remove-account");
50             }
51           });
52         }
53       });
54     }
55   }
56  
57   module.exports = User;

Creating the Main Menu

To make this application more elegant and interactive, we are going to also create in its footer the main menu to help users interact with the tasks list and user settings. To create this screen, first we need to create its template, which will have only three buttons: tasks, add task, and logout. Create the file src/templates/footer.js.

 1   exports.render = path => {
 2     let isTasks = path === "tasks" ? "active" : "";
 3     let isTaskForm = path === "taskForm" ? "active" : "";
 4     let isUser = path === "user" ? "active" : "";
 5     return `
 6       <div class="tabs-striped tabs-color-calm">
 7         <div class="tabs">
 8           <a data-path="tasks" class="tab-item ${isTasks}">
 9             <i class="icon ion-home"></i>
10           </a>
11           <a data-path="taskForm" class="tab-item ${isTaskForm}">
12             <i class="icon ion-compose"></i>
13           </a>
14           <a data-path="user" class="tab-item ${isUser}">
15             <i class="icon ion-person"></i>
16           </a>
17           <a data-logout class="tab-item">
18             <i class="icon ion-android-exit"></i>
19           </a>
20         </div>
21       </div>`;
22   };

Then, create its corresponding component file, src/components/menu.js.

 1   import NTask from "../ntask.js";
 2   import Template from "../templates/footer.js";
 3  
 4   class Menu extends NTask {
 5     constructor(body) {
 6       super();
 7       this.body = body;
 8     }
 9     render(path) {
10       this.body.innerHTML = Template.render(path);
11       this.addEventListener();
12     }
13     clear() {
14       this.body.innerHTML = "";
15     }
16     addEventListener() {
17       this.pathsClick();
18       this.logoutClick();
19     }
20     pathsClick() {
21       const links = this.body.querySelectorAll("[data-path]");
22       for(let i = 0, max = links.length; i < max; i++) {
23         links[i].addEventListener("click", (e) => {
24           e.preventDefault();
25           const link = e.target.parentElement;
26           const path = link.getAttribute("data-path");
27           this.emit("click", path);
28         });
29       }
30     }
31     logoutClick() {
32       const link = this.body.querySelector("[data-logout]");
33       link.addEventListener("click", (e) => {
34         e.preventDefault();
35         this.emit("logout");
36       })
37     }
38   }
39  
40   module.exports = Menu;

Treating All Screen Events

Our project has all the necessary components to build a task list application, now to finish our project we need to assemble all pieces of the puzzle! To start, let’s modify the src/index.js so it can manipulate not only the <main> tag, but also the <footer> tag, because this new tag will be used to handle events of the footer menu.

Edit the src/index.js file, applying this simple modification.

1   import App from "./app.js";
2
3   window.onload = () => {
4     const main = document.querySelector("main");
5     const footer = document.querySelector("footer");
6     new App(main, footer).init();
7   };

Now, to finish our project, we have to update the object App so it can be responsible for loading all the components that were created and treating the events of each component. By making this change we will ensure the correct flow for all screen transitions, the menu transition, and the data traffic between the ntask-api and ntask-web projects. To do this, edit the src/app.js script.

 1   import Tasks from "./components/tasks.js";
 2   import TaskForm from "./components/taskForm.js";
 3   import User from "./components/user.js";
 4   import Signin from "./components/signin.js";
 5   import Signup from "./components/signup.js";
 6   import Menu from "./components/menu.js";
 7  
 8   class App {
 9     constructor(body, footer) {
10       this.signin = new Signin(body);
11       this.signup = new Signup(body);
12       this.tasks = new Tasks(body);
13       this.taskForm = new TaskForm(body);
14       this.user = new User(body);
15       this.menu = new Menu(footer);
16     }
17     init() {
18       this.signin.render();
19       this.addEventListener();
20     }
21     addEventListener() {
22       this.signinEvents();
23       this.signupEvents();
24       this.tasksEvents();
25       this.taskFormEvents();
26       this.userEvents();
27       this.menuEvents();
28     }
29     signinEvents() {
30       this.signin.on("error", () => alert("Authentication error"));
31       this.signin.on("signin", (token) => {
32         localStorage.setItem("token", `JWT ${token}`);
33         this.menu.render("tasks");
34         this.tasks.render();
35       });
36       this.signin.on("signup", () => this.signup.render());
37     }
38     signupEvents(){
39       this.signup.on("error", () => alert("Register error"));
40       this.signup.on("signup", (user) => {
41         alert(`${user.name} you were registered!`);
42         this.signin.render();
43       });
44     }
45     tasksEvents() {
46       this.tasks.on("error", () => alert("Task list error"));
47       this.tasks .on("remove-error", () => alert("Task delete error"));
48       this.tasks.on("update-error", () => alert("Task update error"));
49       this.tasks.on("remove", () => this.tasks.render());
50       this.tasks.on("update", () => this.tasks.render());
51     }
52     taskFormEvents() {
53       this.taskForm.on("error", () => alert("Task register error"));
54       this.taskForm.on("submit", () => {
55         this.menu.render("tasks");
56         this.tasks.render();
57       });
58     }
59     userEvents() {
60       this.user.on("error", () => alert("User load error"));
61       this.user.on("remove-error", () => alert("Cancel account error"));
62       this.user.on("remove-account", () => {
63         alert("So sad! You are leaving us :(");
64         localStorage.clear();
65         this.menu.clear();
66         this.signin.render();
67       });
68     }
69     menuEvents() {
70       this.menu.on("click", (path) => {
71         this.menu.render(path);
72         this[path].render();
73       });
74       this.menu.on("logout", () => {
75         localStorage.clear();
76         this.menu.clear();
77         this.signin.render();
78       })
79     }
80   }
81  
82   module.exports = App;

Phew! It’s done! We built our simple but useful client application to interact with our current API. To test it, you just need to restart the client application and use it normally. Figures 13-1 through 13-3 display some new screens to access.

A435096_1_En_13_Fig1_HTML.jpg
Figure 13-1. Adding a task
A435096_1_En_13_Fig2_HTML.jpg
Figure 13-2. Listing and checking some tasks
A435096_1_En_13_Fig3_HTML.jpg
Figure 13-3. User settings screen

Conclusion

Congratulations! If you reached this far with your application running perfectly, you have successfully completed this book. I hope you’ve learned a lot about the Node.js platform reading this book, and especially about how to build a simple, but useful REST API, because that’s the essence of the book. I believe I have passed the necessary knowledge to you, faithful reader.

Remember that all source code is available on my personal GitHub . Just access this link: github.com/caio ribeiro-pereira/building-apis-with-nodejs .

Thank you very much for reading this book!

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

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