mirror of
https://github.com/microsoft/Web-Dev-For-Beginners.git
synced 2025-08-31 18:32:16 +02:00
Added intial files
This commit is contained in:
306
7-bank-project/1-template-route/translations/README.hi.md
Normal file
306
7-bank-project/1-template-route/translations/README.hi.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Build a Banking App Part 1: HTML Templates and Routes in a Web App
|
||||
|
||||
## Pre-Lecture Quiz
|
||||
|
||||
[Pre-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/41)
|
||||
|
||||
### Introduction
|
||||
|
||||
Since the advent of JavaScript in browsers, websites are becoming more interactive and complex than ever. Web technologies are now commonly used to create fully functional applications that runs directly into a browser that we call [web applications](https://en.wikipedia.org/wiki/Web_application). As Web apps are highly interactive, users do not want to wait for a full page reload every time an action is performed. That's why JavaScript is used to update the HTML directly using the DOM, to provide a smoother user experience.
|
||||
|
||||
In this lesson, we're going to lay out the foundations to create bank web app, using HTML templates to create multiple screens that can be displayed and updated without having to reload the entire HTML page.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
You need a local web server to test the web app we'll build in this lesson. If don't have one, you can install [Node.js](https://nodejs.org) and use the command `npx lite-server` from your project folder. It will create a local web server and open your app in a browser.
|
||||
|
||||
### Preparation
|
||||
|
||||
On your computer, create a folder named `bank` with a file named `index.html` inside it. We'll start from this HTML [boilerplate](https://en.wikipedia.org/wiki/Boilerplate_code):
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bank App</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- This is where you'll work -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTML templates
|
||||
|
||||
If you want to create multiples screens for a web page, one solution would be to create one HTML file for every screen you want to display. However, this solution comes with some inconvenience:
|
||||
|
||||
- You have to reload the entire HTML when switching screen, which can be slow.
|
||||
- It's difficult to share data between the different screens.
|
||||
|
||||
Another approach is have only one HTML file, and define multiple [HTML templates](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) using the `<template>` element. A template is a reusable HTML block that is not displayed by the browser, and needs to be instantiated at runtime using JavaScript.
|
||||
|
||||
### Task
|
||||
|
||||
We'll create a bank app with two screens: the login page and the dashboard. First, let's add in the HTML body a placeholder element that we'll use to instantiate the different screens of our app:
|
||||
|
||||
```html
|
||||
<div id="app">Loading...</div>
|
||||
```
|
||||
|
||||
We're giving it an `id` to make it easier to locate it with JavaScript later.
|
||||
|
||||
> Tip: since the content of this element will be replaced, we can put in a loading message or indicator that will be shown while the app is loading.
|
||||
|
||||
Next, let's add below the HTML template for the login page. For now we'll only put in there a title and a section containing a link that we'll use to perform the navigation.
|
||||
|
||||
```html
|
||||
<template id="login">
|
||||
<h1>Bank App</h1>
|
||||
<section>
|
||||
<a href="/dashboard">Login</a>
|
||||
</section>
|
||||
</template>
|
||||
```
|
||||
|
||||
Then we'll add another HTML template for the dashboard page. This page will contain different sections:
|
||||
|
||||
- A header with a title and a logout link
|
||||
- The current balance of the bank account
|
||||
- A list of transactions, displayed in a table
|
||||
|
||||
```html
|
||||
<template id="dashboard">
|
||||
<header>
|
||||
<h1>Bank App</h1>
|
||||
<a href="/login">Logout</a>
|
||||
</header>
|
||||
<section>
|
||||
Balance: 100$
|
||||
</section>
|
||||
<section>
|
||||
<h2>Transactions</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Object</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
```
|
||||
|
||||
> Tip: when creating HTML templates, if you want to see what it will look like, you can comment out the `<template>` and `</template>` lines by enclosing them with `<!-- -->`.
|
||||
|
||||
✅ Why do you think we use `id` attributes on the templates? Could we use something else like classes?
|
||||
|
||||
## Displaying templates with JavaScript
|
||||
|
||||
If you try your current HTML file in a browser, you'll see that it get stuck displaying `Loading...`. That's because we need to add some JavaScript code to instantiate and display the HTML templates.
|
||||
|
||||
Instantiating a template is usually done in 3 steps:
|
||||
|
||||
1. Retrieve the template element in the DOM, for example using [`document.getElementById`](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById).
|
||||
2. Clone the template element, using [`cloneNode`](https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode).
|
||||
3. Attach it to the DOM under a visible element, for example using [`appendChild`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild).
|
||||
|
||||
✅ Why do we need to clone the template before attaching it to the DOM? What do you think would happen if we skipped this step?
|
||||
|
||||
### Task
|
||||
|
||||
Create a new file named `app.js` in your project folder and import that file in the `<head>` section of your HTML:
|
||||
|
||||
```html
|
||||
<script src="app.js" defer></script>
|
||||
```
|
||||
|
||||
Now in `app.js`, we'll create a new function `updateRoute`:
|
||||
|
||||
```js
|
||||
function updateRoute(templateId) {
|
||||
const template = document.getElementById(templateId);
|
||||
const view = template.content.cloneNode(true);
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = '';
|
||||
app.appendChild(view);
|
||||
}
|
||||
```
|
||||
|
||||
What we do here is exactly the 3 steps described above. We instantiate the template with the id `templateId`, and put its cloned content within our app placeholder. Note that we need to use `cloneNode(true)` to copy the entire subtree of the template.
|
||||
|
||||
Now call this function with one of the template and look at the result.
|
||||
|
||||
```js
|
||||
updateRoute('login');
|
||||
```
|
||||
|
||||
✅ What's the purpose of this code `app.innerHTML = '';`? What happens without it?
|
||||
|
||||
## Creating routes
|
||||
|
||||
When talking about a web app, we call *Routing* the intent to map **URLs** to specific screens that should be displayed. On a web site with multiple HTML files, this is done automatically as the file paths are reflected on the URL. For example, with these files in your project folder:
|
||||
|
||||
```
|
||||
mywebsite/index.html
|
||||
mywebsite/login.html
|
||||
mywebsite/admin/index.html
|
||||
```
|
||||
|
||||
If you create a web server with `mywebsite` as the root, the URL mapping will be:
|
||||
|
||||
```
|
||||
https://site.com --> mywebsite/index.html
|
||||
https://site.com/login.html --> mywebsite/login.html
|
||||
https://site.com/admin/ --> mywebsite/admin/index.html
|
||||
```
|
||||
|
||||
However, for our web app we are using a single HTML file containing all the screens so this default behavior won't help us. We have to create this map manually and perform update the displayed template using JavaScript.
|
||||
|
||||
### Task
|
||||
|
||||
We'll use a simple object to implement a [map](https://en.wikipedia.org/wiki/Associative_array) between URL paths and our templates. Add this object at the top of your `app.js` file.
|
||||
|
||||
```js
|
||||
const routes = {
|
||||
'/login': { templateId: 'login' },
|
||||
'/dashboard': { templateId: 'dashboard' },
|
||||
};
|
||||
```
|
||||
|
||||
Now let's modify a bit the `updateRoute` function. Instead of passing directly the `templateId` as an argument, we want to retrieve it by first looking at the current URL, and then use our map to get the corresponding template ID value. We can use [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname) to get only the path section from the URL.
|
||||
|
||||
```js
|
||||
function updateRoute() {
|
||||
const path = window.location.pathname;
|
||||
const route = routes[path];
|
||||
|
||||
const template = document.getElementById(route.templateId);
|
||||
const view = template.content.cloneNode(true);
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = '';
|
||||
app.appendChild(view);
|
||||
}
|
||||
```
|
||||
|
||||
Here we mapped the routes we declared to the corresponding template. You can try it that it works correctly by changing the URL manually in your browser.
|
||||
|
||||
✅ What happens if you enter an unknown path in the URL? How could we solve this?
|
||||
|
||||
## Adding navigation
|
||||
|
||||
The next step for our app is to add the possibility to navigate between pages without having to change the URL manually. This implies two things:
|
||||
|
||||
1. Updating the current URL
|
||||
2. Updating the displayed template based on the new URL
|
||||
|
||||
We already took care of the second part with the `updateRoute` function, so we have to figure out how to update the current URL.
|
||||
|
||||
We'll have to use JavaScript and more specifically the [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) that allows to update the URL and create a new entry in the browsing history, without reloading the HTML.
|
||||
|
||||
> Note: While the HTML anchor element [`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) can be used on its own to create hyperlinks to different URLs, it will make the browser reload the HTML by default. It is necessary to prevent this behavior when handling routing with custom javascript, using the preventDefault() function on the click event.
|
||||
|
||||
### Task
|
||||
|
||||
Let's create a new function we can use to navigate in our app:
|
||||
|
||||
```js
|
||||
function navigate(path) {
|
||||
window.history.pushState({}, path, window.location.origin + path);
|
||||
updateRoute();
|
||||
}
|
||||
```
|
||||
|
||||
This method first updates the current URL based on the path given, then updates the template. The property `window.location.origin` returns the URL root, allowing us to reconstruct a complete URL from a given path.
|
||||
|
||||
Now that we have this function, we can take care of the problem we have if a path does not match any defined route. We'll modify the `updateRoute` function by adding a fallback to one of the existing route if we can't find a match.
|
||||
|
||||
```js
|
||||
function updateRoute() {
|
||||
const path = window.location.pathname;
|
||||
const route = routes[path];
|
||||
|
||||
if (!route) {
|
||||
return navigate('/login');
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
If a route cannot be found, we'll now redirect to the `login` page.
|
||||
|
||||
Now let's create a function to get the URL when a link is clicked, and to prevent the browser's default link behavior:
|
||||
|
||||
```js
|
||||
function onLinkClick(event) {
|
||||
event.preventDefault();
|
||||
navigate(event.target.href);
|
||||
}
|
||||
```
|
||||
|
||||
Let's complete the navigation system by adding bindings to our *Login* and *Logout* links in the HTML.
|
||||
|
||||
```html
|
||||
<a href="/dashboard" onclick="onLinkClick()">Login</a>
|
||||
...
|
||||
<a href="/login" onclick="onLinkClick()">Logout</a>
|
||||
```
|
||||
|
||||
Using the [`onclick`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick) attribute bind the `click` event to JavaScript code, here the call to the `navigate()` function.
|
||||
|
||||
Try clicking on these links, you should be now able to navigate between the different screens of your app.
|
||||
|
||||
✅ The `history.pushState` method is part of the HTML5 standard and implemented in [all modern browsers](https://caniuse.com/?search=pushState). If you're building a web app for older browsers, there's a trick you can use in place of this API: using a [hash (`#`)](https://en.wikipedia.org/wiki/URI_fragment) before the path you can implement routing that works with regular anchor navigation and does not reload the page, as it's purpose was to create internal links within a page.
|
||||
|
||||
## Handling the browser's back and forward buttons
|
||||
|
||||
Using the `history.pushState` creates new entries in the browser's navigation history. You can check that by holding the *back button* of your browser, it should display something like this:
|
||||
|
||||

|
||||
|
||||
If you try clicking on the back button a few times, you'll see that the current URL changes and the history is updated, but the same template keeps being displayed.
|
||||
|
||||
That's because don't know that we need to call `updateRoute()` every time the history changes. If you take a look at the [`history.pushState` documentation](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState), you can see that if the state changes - meaning that we moved to a different URL - the [`popstate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event) event is triggered. We'll use that to fix that issue.
|
||||
|
||||
### Task
|
||||
|
||||
To make sure the displayed template is updated when the browser history changes, we'll attach a new function that calls `updateRoute()`. We'll do that at the bottom of our `app.js` file:
|
||||
|
||||
```js
|
||||
window.onpopstate = () => updateRoute();
|
||||
updateRoute();
|
||||
```
|
||||
|
||||
> Note: we used an [arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) here to declare our `popstate` event handler for conciseness, but a regular function would work the same.
|
||||
|
||||
Here's a refresher video on arrow functions:
|
||||
|
||||
[](https://youtube.com/watch?v=OP6eEbOj2sc "Arrow Functions")
|
||||
|
||||
> Click the image above for a video about arrow functions.
|
||||
|
||||
Now try to use the back and forward buttons of your browsers, and check that the displayed route is correctly updated this time.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
Add a new template and route for a third page that shows the credits for this app.
|
||||
|
||||
## Post-Lecture Quiz
|
||||
|
||||
[Post-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/42)
|
||||
|
||||
## Review & Self Study
|
||||
|
||||
Routing is one of the surprisingly tricky parts of web development, especially as the web moves from page refresh behaviors to Single Page Application page refreshes. Read a little about [how the Azure Static Web App service](https://docs.microsoft.com/en-us/azure/static-web-apps/routes?WT.mc_id=academic-13441-cxa) handles routing. Can you explain why some of the decisions described on that document are necessary?
|
||||
|
||||
## Assignment
|
||||
|
||||
[Improve the routing](assignment.md)
|
294
7-bank-project/2-forms/translations/README.hi.md
Normal file
294
7-bank-project/2-forms/translations/README.hi.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Build a Banking App Part 2: Build a Login and Registration Form
|
||||
|
||||
## Pre-Lecture Quiz
|
||||
|
||||
[Pre-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/43)
|
||||
|
||||
### Introduction
|
||||
|
||||
In almost all modern web apps, you can create an account to have your own private space. As multiple users can access a web app at the same time, you need a mechanism to store each user personal data separately and select which information to display information. We won't cover how to manage [user identity securely](https://en.wikipedia.org/wiki/Authentication) as it's an extensive topic on its own, but we'll make sure each user is able to create one (or more) bank account on our app.
|
||||
|
||||
In this part we'll use HTML forms to add login and registration to our web app. We'll see how to send the data to a server API programmatically, and ultimately how to define basic validation rules for user inputs.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
You need to have completed the [HTML templates and routing](../1-template-route/README.md) of the web app for this lesson. You also need to install [Node.js](https://nodejs.org) and [run the server API](../api/README.md) locally so you can send data to create accounts.
|
||||
|
||||
You can test that the server is running properly by executing this command in a terminal:
|
||||
|
||||
```sh
|
||||
curl http://localhost:5000/api
|
||||
# -> should return "Bank API v1.0.0" as a result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form and controls
|
||||
|
||||
The `<form>` element encapsulates a section of an HTML document where the user can input and submit data with interactive controls. There are all sorts of user interface (UI) controls that can be used within a form, the most common one being the `<input>` and the `<button>` elements.
|
||||
|
||||
There are a lot of different [types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) of `<input>`, for example to create a field where the user can enter its username you can use:
|
||||
|
||||
```html
|
||||
<input id="username" name="username" type="text">
|
||||
```
|
||||
|
||||
The `name` attribute will be used as the property name when the form data will be sent over. The `id` attribute is used to associate a `<label>` with the form control.
|
||||
|
||||
> Take a look at the whole list of [`<input>` types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) and [other form controls](https://developer.mozilla.org/en-US/docs/Learn/Forms/Other_form_controls) to get an idea of all the native UI elements you can use when building your UI.
|
||||
|
||||
✅ Note that `<input>` is an [empty element](https://developer.mozilla.org/en-US/docs/Glossary/Empty_element) on which you should *not* add a matching closing tag. You can however use the self-closing `<input/>` notation, but it's not required.
|
||||
|
||||
The `<button>` element within a form is a bit special. If you do not specify its `type` attribute, it will automatically submit the form data to the server when pressed. Here are the possible `type` values:
|
||||
|
||||
- `submit`: The default within a `<form>`, the button triggers the form submit action.
|
||||
- `reset`: The button resets all the form controls to their initial values.
|
||||
- `button`: Do not assign a default behavior when the button is pressed. You can then assign custom actions to it using JavaScript.
|
||||
|
||||
### Task
|
||||
|
||||
Let's start by adding a form to the `login` template. We'll need a *username* field and a *Login* button.
|
||||
|
||||
```html
|
||||
<template id="login">
|
||||
<h1>Bank App</h1>
|
||||
<section>
|
||||
<h2>Login</h2>
|
||||
<form id="loginForm">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="user" type="text">
|
||||
<button>Login</button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
```
|
||||
|
||||
If you take a closer look, you can notice that we also added a `<label>` element here. `<label>` elements are used to add a name to UI controls, such as our username field. Labels are important for the readability of your forms, but also comes with additional benefits:
|
||||
|
||||
- By associating a label to a form control, it helps users using assistive technologies (like a screen reader) to understand what data they're expected to provide.
|
||||
- You can click on the label to directly put focus on the associated input, making it easier to reach on touch-screen based devices.
|
||||
|
||||
> [Accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility) on the web is a very important topic that's often overlooked. Thanks to [semantic HTML elements](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML) it's not difficult to create accessible content if you use them properly. You can [read more about accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility) to avoid common mistakes and become a responsible developer.
|
||||
|
||||
Now we'll add a second form for the registration, just below the previous one:
|
||||
|
||||
```html
|
||||
<hr/>
|
||||
<h2>Register</h2>
|
||||
<form id="registerForm">
|
||||
<label for="user">Username</label>
|
||||
<input id="user" name="user" type="text">
|
||||
<label for="currency">Currency</label>
|
||||
<input id="currency" name="currency" type="text" value="$">
|
||||
<label for="description">Description</label>
|
||||
<input id="description" name="description" type="text">
|
||||
<label for="balance">Current balance</label>
|
||||
<input id="balance" name="balance" type="number" value="0">
|
||||
<button>Register</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
Using the `value` attribute we can define a default value for a given input.
|
||||
Notice also that the input for `balance` has the `number` type. Does it look different than the other inputs? Try interacting with it.
|
||||
|
||||
✅ Can you navigate and interact with the forms using only a keyboard? How would you do that?
|
||||
|
||||
## Submitting data to the server
|
||||
|
||||
Now that we have a functional UI, the next step is to send the data over to our server. Let's make a quick test using our current code: what happens if you click on the *Login* or *Register* button?
|
||||
|
||||
Did you notice the change in your browser's URL section?
|
||||
|
||||

|
||||
|
||||
The default action for a `<form>` is to submit the form to the current server URL using the [GET method](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3), appending the form data directly to the URL. This method has some shortcomings though:
|
||||
|
||||
- The data sent is very limited in size (about 2000 characters)
|
||||
- The data is directly visible in the URL (not great for passwords)
|
||||
- It does not work with file uploads
|
||||
|
||||
That's why you can change it to use the [POST method](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) which sends the form data to the server in the body of the HTTP request, without any of the previous limitations.
|
||||
|
||||
> While POST is the most commonly used method to send data over, [in some specific scenarios](https://www.w3.org/2001/tag/doc/whenToUseGet.html) it is preferable to use the GET method, when implementing a search field for example.
|
||||
|
||||
### Task
|
||||
|
||||
Add `action` and `method` properties to the registration form:
|
||||
|
||||
```html
|
||||
<form id="registerForm" action="//localhost:5000/api/accounts" method="POST">
|
||||
```
|
||||
|
||||
Now try to register a new account with your name. After clicking on the *Register* button you should see something like this:
|
||||
|
||||

|
||||
|
||||
If everything goes well, the server should answer your request with a [JSON](https://www.json.org/json-en.html) response containing the account data that was created.
|
||||
|
||||
✅ Try registering again with the same name. What happens?
|
||||
|
||||
## Submitting data without reloading the page
|
||||
|
||||
As you probably noticed, there's a slight issue with the approach we just used: when submitting the form, we get out of our app and the browser redirects to the server URL. We're trying to avoid all page reloads with our web app, as we're makng a [Single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application).
|
||||
|
||||
To send the form data to the server without forcing a page reload, we have to use JavaScript code. Instead of putting an URL in the `action` property of a `<form>` element, you can use any JavaScript code prepended by the `javascript:` string to perform a custom action. Using this also means that you'll have to implement some tasks that were previously done automatically by the browser:
|
||||
|
||||
- Retrieve the form data
|
||||
- Convert and encode the form data to a suitable format
|
||||
- Create the HTTP request and send it to the server
|
||||
|
||||
### Task
|
||||
|
||||
Replace the registration form `action` with:
|
||||
|
||||
```html
|
||||
<form id="registerForm" action="javascript:register()">
|
||||
```
|
||||
|
||||
Open `app.js` add a new function named `register`:
|
||||
|
||||
```js
|
||||
function register() {
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
const formData = new FormData(registerForm);
|
||||
const data = Object.fromEntries(formData);
|
||||
const jsonData = JSON.stringify(data);
|
||||
}
|
||||
```
|
||||
|
||||
Here we retrieve the form element using `getElementById()` and use the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) helper to extract the values from form controls as a set of key/value pairs. Then we convert the data to a regular object using [`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) and finally serialize the data to [JSON](https://www.json.org/json-en.html), a format commonly used for exchanging data on the web.
|
||||
|
||||
The data is now ready to be sent to the server. Create a new function named `createAccount`:
|
||||
|
||||
```js
|
||||
async function createAccount(account) {
|
||||
try {
|
||||
const response = await fetch('//localhost:5000/api/accounts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: account
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { error: error.message || 'Unknown error' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
What's this function doing? First, notice the `async` keyword here. This means that the function contains code that will execute [**asynchronously**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function). When used along the `await` keyword, it allows waiting for asynchronous code to execute - like waiting for the server response here - before continuing.
|
||||
|
||||
Here's a quick video about `async/await` usage:
|
||||
|
||||
[](https://youtube.com/watch?v=YwmlRkrxvkk "Async and Await for managing promises")
|
||||
|
||||
> Click the image above for a video about async/await.
|
||||
|
||||
We use the `fetch()` API to send JSON data to the server. This method takes 2 parameters:
|
||||
|
||||
- The URL of the server, so we put back `//localhost:5000/api/accounts` here.
|
||||
- The settings of the request. That's where we set the method to `POST` and provide the `body` for the request. As we're sending JSON data to the server, we also need to set the `Content-Type` header to `application/json` so the server know how to interpret the content.
|
||||
|
||||
As the server will respond to the request with JSON, we can use `await response.json()` to parse the JSON content and return the resulting object. Note that this method is asynchronous, so we use the `await` keyword here before returning to make sure any errors during parsing are also caught.
|
||||
|
||||
Now add some code to the `register` function to call `createAccount()`:
|
||||
|
||||
```js
|
||||
const result = await createAccount(jsonData);
|
||||
```
|
||||
|
||||
Because we use the `await` keyword here, we need to add the `async` keyword before the register function:
|
||||
|
||||
```js
|
||||
async function register() {
|
||||
```
|
||||
|
||||
Finally, let's add some logs to check the result. The final function should look like this:
|
||||
|
||||
```js
|
||||
async function register() {
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
const formData = new FormData(registerForm);
|
||||
const jsonData = JSON.stringify(Object.fromEntries(formData));
|
||||
const result = await createAccount(jsonData);
|
||||
|
||||
if (result.error) {
|
||||
return console.log('An error occured:', result.error);
|
||||
}
|
||||
|
||||
console.log('Account created!', result);
|
||||
}
|
||||
```
|
||||
|
||||
That was a bit long but we got there! If you open your [browser developer tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools), and try registering a new account, you should not see any change on the web page but a message will appear in the console confirming that everything works.
|
||||
|
||||

|
||||
|
||||
✅ Do you think the data is sent to the server securely? What if someone what was able to intercept the request? You can read about [HTTPS](https://en.wikipedia.org/wiki/HTTPS) to know more about secure data communication.
|
||||
|
||||
## Data validation
|
||||
|
||||
If you try to register a new account without setting an username first, you can see that the server returns an error with status code [400 (Bad Request)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400#:~:text=The%20HyperText%20Transfer%20Protocol%20(HTTP,%2C%20or%20deceptive%20request%20routing).).
|
||||
|
||||
Before sending data to a server it's a good practice to [validate the form data](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation) beforehand when possible, to make sure you send a valid request. HTML5 forms controls provides built-in validation using various attributes:
|
||||
|
||||
- `required`: the field needs to be filled otherwise the form cannot be submitted.
|
||||
- `minlength` and `maxlength`: defines the minimum and maximum number of characters in text fields.
|
||||
- `min` and `max`: defines the minimum and maximum value of a numerical field.
|
||||
- `type`: defines the kind of data expected, like `number`, `email`, `file` or [other built-in types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). This attribute may also change the visual rendering of the form control.
|
||||
- `pattern`: allows to define a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) pattern to test if the entered data is valid or not.
|
||||
|
||||
> Tip: you can customize the look of your form controls depending if they're valid or not using the `:valid` and `:invalid` CSS pseudo-classes.
|
||||
|
||||
### Task
|
||||
|
||||
There are 2 required fields to create a valid new account, the username and currency, the other fields being optional. Update the form's HTML, using both the `required` attribute and text in the field's label to that:
|
||||
|
||||
```html
|
||||
<label for="user">Username (required)</label>
|
||||
<input id="user" name="user" type="text" required>
|
||||
...
|
||||
<label for="currency">Currency (required)</label>
|
||||
<input id="currency" name="currency" type="text" value="$" required>
|
||||
```
|
||||
|
||||
While this particular server implementation does not enforce specific limits on the fields maximum length, it's always a good practice to define reasonable limits for any user text entry.
|
||||
|
||||
Add a `maxlength` attribute to the text fields:
|
||||
|
||||
```html
|
||||
<input id="user" name="user" type="text" maxlength="20" required>
|
||||
...
|
||||
<input id="currency" name="currency" type="text" value="$" maxlength="5" required>
|
||||
...
|
||||
<input id="description" name="description" type="text" maxlength="100">
|
||||
```
|
||||
|
||||
Now if you press the *Register* button and a field does not respect a validation rule we defined, you should see something like this:
|
||||
|
||||

|
||||
|
||||
Validation like this performed *before* sending any data to the server is called **client-side** validation. But note that's it's not always possible to peform all checks without sending the data. For example, we cannot check here if an account already exists with the same username without sending a request to the server. Additional validation performed on the server is called **server-side** validation.
|
||||
|
||||
Usually both need to be implemented, and while using client-side validation improves the user experience by providing instant feedback to the user, server-side validation is crucial to make sure the user data you manipulate is sound and safe.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
Show an error message in the HTML if the user already exists.
|
||||
|
||||
Here's an example of what the final login page can look like after a bit of styling:
|
||||
|
||||

|
||||
|
||||
## Post-Lecture Quiz
|
||||
|
||||
[Post-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/44)
|
||||
|
||||
## Review & Self Study
|
||||
|
||||
Developers have gotten very creative about their form building efforts, especially regarding validation strategies. Learn about different form flows by looking through [CodePen](https://codepen.com); can you find some interesting and inspiring forms?
|
||||
|
||||
## Assignment
|
||||
|
||||
[Style your bank app](assignment.md)
|
334
7-bank-project/3-data/translations/README.hi.md
Normal file
334
7-bank-project/3-data/translations/README.hi.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Build a Banking App Part 3: Methods of Fetching and Using Data
|
||||
|
||||
## Pre-Lecture Quiz
|
||||
|
||||
[Pre-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/45)
|
||||
|
||||
### Introduction
|
||||
|
||||
At the core of every web application there's *data*. Data can take many forms, but its main purpose is always to display information to the user. With web apps becoming increasingly interactive and complex, how the user accesses and interacts with information is now a key part of web development.
|
||||
|
||||
In this lesson, we'll see how to fetch data from a server asynchronously, and use this data to display information on a web page without reloading the HTML.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
You need to have built the [Login and Registration Form](../2-forms/README.md) part of the web app for this lesson. You also need to install [Node.js](https://nodejs.org) and [run the server API](../api/README.md) locally so you get account data.
|
||||
|
||||
You can test that the server is running properly by executing this command in a terminal:
|
||||
|
||||
```sh
|
||||
curl http://localhost:5000/api
|
||||
# -> should return "Bank API v1.0.0" as a result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AJAX and data fetching
|
||||
|
||||
Traditional web sites update the content displayed when the user selects a link or submits data using a form, by reloading the full HTML page. Every time new data needs to be loaded, the web server returns a brand new HTML page that needs to be processed by the browser, interrupting the current user action and limiting interactions during the reload. This workflow is also called a *Multi-Page Application* or *MPA*.
|
||||
|
||||

|
||||
|
||||
When web applications started to become more complex and interactive, a new technique called [AJAX (Asynchronous JavaScript and XML)](https://en.wikipedia.org/wiki/Ajax_(programming)) emerged. This technique allows web apps to send and retrieve data from a server asynchronously using JavaScript, without having to reload the HTML page, resulting in faster updates and smoother user interactions. When new data is received from the server, the current HTML page can also be updated with JavaScript using the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) API. Over time, this approach has evolved into what is now called a [*Single-Page Application* or *SPA*](https://en.wikipedia.org/wiki/Single-page_application).
|
||||
|
||||

|
||||
|
||||
When AJAX was first introduced, the only API available to fetch data asynchronously was [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest). But modern browsers now also implement the more convenient and powerful [`Fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), which uses promises and is better suited to manipulate JSON data.
|
||||
|
||||
> While all modern browsers support the `Fetch API`, if you want your web application to work on legacy or old browsers it's always a good idea to check the [compatibility table on caniuse.com](https://caniuse.com/fetch) first.
|
||||
|
||||
### Task
|
||||
|
||||
In [the previous lesson](../2-forms/README.md) we implemented the registration form to create an account. We'll now add code to login using an existing account, and fetch its data. Open the `app.js` file and add a new `login` function:
|
||||
|
||||
```js
|
||||
async function login() {
|
||||
const loginForm = document.getElementById('loginForm')
|
||||
const user = loginForm.user.value;
|
||||
}
|
||||
```
|
||||
|
||||
Here we start by retrieving the form element with `getElementById()`, and then we get the username from the input with `loginForm.user.value`. Every form control can be accessed by its name (set in the HTML using the `name` attribute) as a property of the form.
|
||||
|
||||
In a similar fashion to what we did for the registration, we'll create another function to perform a server request, but this time for retrieving the account data:
|
||||
|
||||
```js
|
||||
async function getAccount(user) {
|
||||
try {
|
||||
const response = await fetch('//localhost:5000/api/accounts/' + encodeURIComponent(user));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { error: error.message || 'Unknown error' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We use the `fetch` API to request the data asynchronously from the server, but this time we don't need any extra parameters other than the URL to call, as we're only querying data. By default, `fetch` creates a [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) HTTP request, which is what we are seeking here.
|
||||
|
||||
✅ `encodeURIComponent()` is a function that escapes special characters for URL. What issues could we possibly have if we do not call this function and use directly the `user` value in the URL?
|
||||
|
||||
Let's now update our `login` function to use `getAccount`:
|
||||
|
||||
```js
|
||||
async function login() {
|
||||
const loginForm = document.getElementById('loginForm')
|
||||
const user = loginForm.user.value;
|
||||
const data = await getAccount(user);
|
||||
|
||||
if (data.error) {
|
||||
return console.log('loginError', data.error);
|
||||
}
|
||||
|
||||
account = data;
|
||||
navigate('/dashboard');
|
||||
}
|
||||
```
|
||||
|
||||
First, as `getAccount` is an asynchronous function we need to match it with the `await` keyword to wait for the server result. As with any server request, we also have to deal with error cases. For now we'll only add a log message to display the error, and come back to it later.
|
||||
|
||||
Then we have to store the data somewhere so we can later use it to display the dashboard informations. Since the `account` variable does not exist yet, we'll create a global variable for it at the top of our file:
|
||||
|
||||
```js
|
||||
let account = null;
|
||||
```
|
||||
|
||||
After the user data is saved into a variable we can navigate from the *login* page to the *dashboard* using the `navigate()` function we already have.
|
||||
|
||||
Finally, we need to call our `login` function when the login form is submitted, by modifying the HTML:
|
||||
|
||||
```html
|
||||
<form id="loginForm" action="javascript:login()">
|
||||
```
|
||||
|
||||
Test that everything is working correctly by registering a new account and trying to login using the same account.
|
||||
|
||||
Before moving on to the next part, we can also complete the `register` function by adding this at the bottom of the function:
|
||||
|
||||
```js
|
||||
account = result;
|
||||
navigate('/dashboard');
|
||||
```
|
||||
|
||||
✅ Did you know that by default, you can only call server APIs from the *same domain and port* than the web page you are viewing? This is security mechanism enforced by browsers. But wait, our web app is running on `localhost:3000` whereas the server API is running on ` localhost:5000`, why does it work? By using a technique called [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), it is possible to perform cross-origin HTTP requests if the server adds special headers to the response, allowing exceptions for specific domains.
|
||||
|
||||
> Learn more about APIs by taking this [lesson](https://docs.microsoft.com/en-us/learn/modules/use-apis-discover-museum-art?WT.mc_id=academic-13441-cxa)
|
||||
|
||||
## Update HTML to display data
|
||||
|
||||
Now that we have the user data, we have to update the existing HTML to display it. We already know how to retrieve an element from the DOM using for example `document.getElementById()`. After you have a base element, here are some APIs you can use to modify it or add child elements to it:
|
||||
|
||||
- Using the [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) property you can change the text of an element. Note that changing this value removes all the element's children (if there's any) and replaces it with the text provided. As such, it's also an efficient method to remove all children of a given element by assigning an empty string `''` to it.
|
||||
|
||||
- Using [`document.createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) along with the [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append) method you can create and attach one or more new child elements.
|
||||
|
||||
✅ Using the [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) property of an element it's also possible to change its HTML contents, but this one should be avoided as it's vulnerable to [cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) attacks.
|
||||
|
||||
### Task
|
||||
|
||||
Before moving on to the dashboard screen, there's one more thing we should do on the *login* page. Currently, if you try to login with a username that does not exist, a message is shown in the console but for a normal user nothing changes and you don't know what's going on.
|
||||
|
||||
Let's add a placeholder element in the login form where we can display an error message if needed. A good place would be just before the login `<button>`:
|
||||
|
||||
```html
|
||||
...
|
||||
<div id="loginError"></div>
|
||||
<button>Login</button>
|
||||
...
|
||||
```
|
||||
|
||||
This `<div>` element is empty, meaning that nothing will be displayed on the screen until we add some content to it. We also give it an `id` so we can retrieve it easily with JavaScript.
|
||||
|
||||
Go back to the `app.js` file and create a new helper function `updateElement`:
|
||||
|
||||
```js
|
||||
function updateElement(id, text) {
|
||||
const element = document.getElementById(id);
|
||||
element.textContent = text;
|
||||
}
|
||||
```
|
||||
|
||||
This one is quite straightforward: given an element *id* and *text*, it will update the text content of the DOM element with the matching `id`. Let's use this method in place of the previous error message in the `login` function:
|
||||
|
||||
```js
|
||||
if (data.error) {
|
||||
return updateElement('loginError', data.error);
|
||||
}
|
||||
```
|
||||
|
||||
Now if you try to login with an invalid account, you should see something like this:
|
||||
|
||||

|
||||
|
||||
Now we have error text that shows up visually, but if you try it with a screen reader you'll notice that nothing is announced. In order for text that is dynamically added to a page to be announced by screen readers, it will need to use something called a [Live Region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). Here we're going to use a specific type of live region called an alert:
|
||||
|
||||
```html
|
||||
<div id="loginError" role="alert"></div>
|
||||
```
|
||||
|
||||
Implement the same behavior for the `register` function errors (don't forget to update the HTML).
|
||||
|
||||
## Display information on the dashboard
|
||||
|
||||
Using the same techniques we've just seen, we'll also take care of displaying the account information on the dashboard page.
|
||||
|
||||
This is what an account object received from the server looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"user": "test",
|
||||
"currency": "$",
|
||||
"description": "Test account",
|
||||
"balance": 75,
|
||||
"transactions": [
|
||||
{ "id": "1", "date": "2020-10-01", "object": "Pocket money", "amount": 50 },
|
||||
{ "id": "2", "date": "2020-10-03", "object": "Book", "amount": -10 },
|
||||
{ "id": "3", "date": "2020-10-04", "object": "Sandwich", "amount": -5 }
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
> Note: to make your life easier, you can use the pre-existing `test` account that's already populated with data.
|
||||
|
||||
### Task
|
||||
|
||||
Let's start by replacing the "Balance" section in the HTML to add placeholder elements:
|
||||
|
||||
```html
|
||||
<section>
|
||||
Balance: <span id="balance"></span><span id="currency"></span>
|
||||
</section>
|
||||
```
|
||||
|
||||
We'll also add a new section just below to display the account description:
|
||||
|
||||
```html
|
||||
<h2 id="description"></h2>
|
||||
```
|
||||
|
||||
✅ Since the account description functions as a title for the content underneath it, it is marked up semantically as a heading. Learn more about how [heading structure](https://www.nomensa.com/blog/2017/how-structure-headings-web-accessibility) is important for accessibility, and take a critical look at the page to determine what else could be a heading.
|
||||
|
||||
Next, we'll create a new function in `app.js` to fill in the placeholder:
|
||||
|
||||
```js
|
||||
function updateDashboard() {
|
||||
if (!account) {
|
||||
return navigate('/login');
|
||||
}
|
||||
|
||||
updateElement('description', account.description);
|
||||
updateElement('balance', account.balance.toFixed(2));
|
||||
updateElement('currency', account.currency);
|
||||
}
|
||||
```
|
||||
|
||||
First, we check that we have the account data we need before going further. Then we use the `updateElement()` function we created earlier to update the HTML.
|
||||
|
||||
> To make the balance display prettier, we use the method [`toFixed(2)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) to force displaying the value with 2 digits after the decimal point.
|
||||
|
||||
Now we need to call our `updateDashboard()` function everytime the dashboard is loaded. If you already finished the [lesson 1 assignment](../1-template-route/assignment.md) this should be straighforward, otherwise you can use the following implementation.
|
||||
|
||||
Add this code to the end of the `updateRoute()` function:
|
||||
|
||||
```js
|
||||
if (typeof route.init === 'function') {
|
||||
route.init();
|
||||
}
|
||||
```
|
||||
|
||||
And update the routes definition with:
|
||||
|
||||
```js
|
||||
const routes = {
|
||||
'/login': { templateId: 'login' },
|
||||
'/dashboard': { templateId: 'dashboard', init: updateDashboard }
|
||||
};
|
||||
```
|
||||
|
||||
With this change, every time the dashboard page is displayed, the function `updateDashboard()` is called. After a login, you should then be able to see the account balance, currency and description.
|
||||
|
||||
## Create table rows dynamically with HTML templates
|
||||
|
||||
In the [first lesson](../1-template-route/README.md) we used HTML templates along with the [`appendChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) method to implement the navigation in our app. Templates can also be smaller and used to dynamically populate repetitive parts of a page.
|
||||
|
||||
We'll use a similar approach to display the list of transactions in the HTML table.
|
||||
|
||||
### Task
|
||||
|
||||
Add a new template in the HTML `<body>`:
|
||||
|
||||
```html
|
||||
<template id="transaction">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</template>
|
||||
```
|
||||
|
||||
This template represents a single table row, with the 3 columns we want to populate: *date*, *object* and *amount* of a transaction.
|
||||
|
||||
Then, add this `id` property to the `<tbody>` element of the table within the dashboard template to make it easier to find using JavaScript:
|
||||
|
||||
```html
|
||||
<tbody id="transactions"></tbody>
|
||||
```
|
||||
|
||||
Our HTML is ready, let's switch to JavaScript code and create a new function `createTransactionRow`:
|
||||
|
||||
```js
|
||||
function createTransactionRow(transaction) {
|
||||
const template = document.getElementById('transaction');
|
||||
const transactionRow = template.content.cloneNode(true);
|
||||
const tr = transactionRow.querySelector('tr');
|
||||
tr.children[0].textContent = transaction.date;
|
||||
tr.children[1].textContent = transaction.object;
|
||||
tr.children[2].textContent = transaction.amount.toFixed(2);
|
||||
return transactionRow;
|
||||
}
|
||||
```
|
||||
|
||||
This function does exactly what its names implies: using the template we created earlier, it creates a new table row and fills in its contents using transaction data. We'll use this in our `updateDashboard()` function to populate the table:
|
||||
|
||||
```js
|
||||
const transactionsRows = document.createDocumentFragment();
|
||||
for (const transaction of account.transactions) {
|
||||
const transactionRow = createTransactionRow(transaction);
|
||||
transactionsRows.appendChild(transactionRow);
|
||||
}
|
||||
updateElement('transactions', transactionsRows);
|
||||
```
|
||||
|
||||
Here we use the method [`document.createDocumentFragment()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createDocumentFragment) that creates a new DOM fragment on which we can work, before finally attaching it to our HTML table.
|
||||
|
||||
There's still one more thing we have to do before this code can work, as our `updateElement()` function currently supports text content only. Let's change its code a bit:
|
||||
|
||||
```js
|
||||
function updateElement(id, textOrNode) {
|
||||
const element = document.getElementById(id);
|
||||
element.textContent = ''; // Removes all children
|
||||
element.append(textOrNode);
|
||||
}
|
||||
```
|
||||
|
||||
We use the [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append) method as it allows to attach either text or [DOM Nodes](https://developer.mozilla.org/en-US/docs/Web/API/Node) to a parent element, which is perfect for all our use cases.
|
||||
|
||||
If you try using the `test` account to login, you should now see a transaction list on the dashboard 🎉.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
Work together to make the dashboard page look like a real banking app. If you already styled your app, try to use [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries) to create a [responsive design](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Responsive/responsive_design_building_blocks) working nicely on both desktop and mobile devices.
|
||||
|
||||
Here's an example of a styled dashboard page:
|
||||
|
||||

|
||||
|
||||
## Post-Lecture Quiz
|
||||
|
||||
[Post-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/46)
|
||||
|
||||
## Assignment
|
||||
|
||||
[Refactor and comment your code](assignment.md)
|
281
7-bank-project/4-state-management/translations/README.hi.md
Normal file
281
7-bank-project/4-state-management/translations/README.hi.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Build a Banking App Part 4: Concepts of State Management
|
||||
|
||||
## Pre-Lecture Quiz
|
||||
|
||||
[Pre-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/47)
|
||||
|
||||
### Introduction
|
||||
|
||||
As a web application grows, it becomes a challenge to keep track of all data flows. Which code gets the data, what page consumes it, where and when does it need to be updated...it's easy to end up with messy code that's difficult to maintain. This is especially true when you need to share data among different pages of your app, for example user data. The concept of *state management* has always existed in all kinds of programs, but as web apps keep growing in complexity it's now a key point to think about during development.
|
||||
|
||||
In this final part, we'll look over the app we built to rethink how the state is managed, allowing support for browser refresh at any point, and persisting data across user sessions.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
You need to have completed the [data fetching](../3-data/README.md) part of the web app for this lesson. You also need to install [Node.js](https://nodejs.org) and [run the server API](../api/README.md) locally so you can manage account data.
|
||||
|
||||
You can test that the server is running properly by executing this command in a terminal:
|
||||
|
||||
```sh
|
||||
curl http://localhost:5000/api
|
||||
# -> should return "Bank API v1.0.0" as a result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rethink state management
|
||||
|
||||
In the [previous lesson](../3-data/README.md), we introduced a basic concept of state in our app with the global `account` variable which contains the bank data for the currently logged in user. However, our current implementation has some flaws. Try refreshing the page when you're on the dashboard. What happens?
|
||||
|
||||
There's 3 issues with the current code:
|
||||
|
||||
- The state is not persisted, as a browser refresh takes you back to the login page.
|
||||
- There are multiple functions that modify the state. As the app grows, it can make it difficult to track the changes and it's easy to forget updating one.
|
||||
- The state is not cleaned up, so when you click on *Logout* the account data is still there even though you're on the login page.
|
||||
|
||||
We could update our code to tackle these issues one by one, but it would create more code duplication and make the app more complex and difficult to maintain. Or we could pause for a few minutes and rethink our strategy.
|
||||
|
||||
> What problems are we really trying to solve here?
|
||||
|
||||
[State management](https://en.wikipedia.org/wiki/State_management) is all about finding a good approach to solve these two particular problems:
|
||||
|
||||
- How to keep the data flows in an app understandable?
|
||||
- How to keep the state data always in sync with the user interface (and vice versa)?
|
||||
|
||||
Once you've taken care of these, any other issues you might have may either be fixed already or have become easier to fix. There are many possible approaches for solving these problems, but we'll go with a common solution that consists of **centralizing the data and the ways to change it**. The data flows would go like this:
|
||||
|
||||

|
||||
|
||||
> We won't cover here the part where the data automatically triggers the view update, as it's tied to more advanced concepts of [Reactive Programming](https://en.wikipedia.org/wiki/Reactive_programming). It's a good follow-up subject if you're up to a deep dive.
|
||||
|
||||
✅ There are a lot of libraries out there with different approaches to state management, [Redux](https://redux.js.org) being a popular option. Take a look at the concepts and patterns used as it's often a good way to learn what potential issues you may be facing in large web apps and how it can be solved.
|
||||
|
||||
### Task
|
||||
|
||||
We'll start with a bit of refactoring. Replace the `account` declaration:
|
||||
|
||||
```js
|
||||
let account = null;
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```js
|
||||
let state = {
|
||||
account: null
|
||||
};
|
||||
```
|
||||
|
||||
The idea is to *centralize* all our app data in a single state object. We only have `account` for now in the state so it doesn't change much, but it creates a path for evolutions.
|
||||
|
||||
We also have to update the functions using it. In the `register()` and `login()` functions, replace `account = ...` with `state.account = ...`;
|
||||
|
||||
At the top of the `updateDashboard()` function, add this line:
|
||||
|
||||
```js
|
||||
const account = state.account;
|
||||
```
|
||||
|
||||
This refactoring by itself did not bring much improvements, but the idea was to lay out the foundation for the next changes.
|
||||
|
||||
## Track data changes
|
||||
|
||||
Now that we have put in place the `state` object to store our data, the next step is centralize the updates. The goal is to make it easier to keep track of any changes and when they happen.
|
||||
|
||||
To avoid having changes made to the `state` object, it's also a good practice to consider it [*immutable*](https://en.wikipedia.org/wiki/Immutable_object), meaning that it cannot be modified at all. It also means that you have to create a new state object if you want to change anything in it. By doing this, you build a protection about potentially unwanted [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)), and open up possibilities for new features in your app like implementing undo/redo, while also making it easier to debug. For example, you could log every change made to the state and keep a history of the changes to understand the source of a bug.
|
||||
|
||||
In JavaScript, you can use [`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) to create an immutable version of an object. If you try to make changes to an immutable object, an exception will be raised.
|
||||
|
||||
✅ Do you know the difference between a *shallow* and a *deep* immutable object? You can read about it [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze).
|
||||
|
||||
### Task
|
||||
|
||||
Let's create a new `updateState()` function:
|
||||
|
||||
```js
|
||||
function updateState(property, newData) {
|
||||
state = Object.freeze({
|
||||
...state,
|
||||
[property]: newData
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In this function, we're creating a new state object and copy data from the previous state using the [*spread (`...`) operator*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals). Then we override a particular property of the state object with the new data using the [bracket notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties) `[property]` for assignment. Finally, we lock the object to prevent modifications using `Object.freeze()`. We only have the `account` property stored in the state for now, but with this approach you can add as many properties as you need in the state.
|
||||
|
||||
We'll also update the `state` initialization to make sure the initial state is frozen too:
|
||||
|
||||
```js
|
||||
let state = Object.freeze({
|
||||
account: null
|
||||
});
|
||||
```
|
||||
|
||||
After that, update the `register` function by replacing the `state.account = result;` assignment with:
|
||||
|
||||
```js
|
||||
updateState('account', result);
|
||||
```
|
||||
|
||||
Do the same with the `login` function, replacing `state.account = data;` with:
|
||||
|
||||
```js
|
||||
updateState('account', data);
|
||||
```
|
||||
|
||||
We'll now take the chance to fix the issue of account data not being cleared when the user clicks on *Logout*.
|
||||
|
||||
Create a new function `logout()`:
|
||||
|
||||
```js
|
||||
function logout() {
|
||||
updateState('account', null);
|
||||
navigate('/login');
|
||||
}
|
||||
```
|
||||
|
||||
In `updateDashboard()`, replace the redirection `return navigate('/login');` with `return logout()`;
|
||||
|
||||
Try registering a new account, logging out and in again to check that everything still works correctly.
|
||||
|
||||
> Tip: you can take a look at all state changes by adding `console.log(state)` at the bottom of `updateState()` and opening up the console in your browser's development tools.
|
||||
|
||||
## Persist the state
|
||||
|
||||
Most web apps needs to persist data to be able to work correctly. All the critical data is usually stored on a database and accessed via a server API, like as the user account data in our case. But sometimes, it's also interesting to persist some data on the client app that's running in your browser, for a better user experience or to improve loading performance.
|
||||
|
||||
When you want to persist data in your browser, there are a few important questions you should ask yourself:
|
||||
|
||||
- *Is the data sensitive?* You should avoid storing any sensitive data on client, such as user passwords.
|
||||
- *For how long do you need to keep this data?* Do you plan to access this data only for the current session or do you want it to be stored forever?
|
||||
|
||||
There are multiple ways of storing information inside a web app, depending on what you want to achieve. For example, you can use the URLs to store a search query, and make it shareable between users. You can also use [HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) if the data needs to be shared with the server, like [authentication](https://en.wikipedia.org/wiki/Authentication) information.
|
||||
|
||||
Another option is to use one of the many browser APIs for storing data. Two of them are particularly interesting:
|
||||
|
||||
- [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage): a [Key/Value store](https://en.wikipedia.org/wiki/Key%E2%80%93value_database) allowing to persist data specific to the current web site across different sessions. The data saved in it never expires.
|
||||
- [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage): this one is works the same as `localStorage` except that the data stored in it is cleared when the session ends (when the browser is closed).
|
||||
|
||||
Note that both these APIs only allow to store [strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String). If you want to store complex objects, you will need to serialize it to the [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) format using [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
|
||||
|
||||
✅ If you want to create a web app that does not work with a server, it's also possible to create a database on the client using the [`IndexedDB` API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). This one is reserved for advanced use cases or if you need to store significant amount of data, as it's more complex to use.
|
||||
|
||||
### Task
|
||||
|
||||
We want our users stay logged in until they explicitly click on the *Logout* button, so we'll use `localStorage` to store the account data. First, let's define a key that we'll use to store our data.
|
||||
|
||||
```js
|
||||
const storageKey = 'savedAccount';
|
||||
```
|
||||
|
||||
Then add this line at the end of the `updateState()` function:
|
||||
|
||||
```js
|
||||
localStorage.setItem(storageKey, JSON.stringify(state.account));
|
||||
```
|
||||
|
||||
With this, the user account data will be persisted and always up-to-date as we centralized previously all our state updates. This is where we begin to benefit from all our previous refactors 🙂.
|
||||
|
||||
As the data is saved, we also have to take care of restoring it when the app is loaded. Since we'll begin to have more initialization code it may be a good idea to create a new `init` function, that also includes our previous code at the bottom of `app.js`:
|
||||
|
||||
```js
|
||||
function init() {
|
||||
const savedAccount = localStorage.getItem(storageKey);
|
||||
if (savedAccount) {
|
||||
updateState('account', JSON.parse(savedAccount));
|
||||
}
|
||||
|
||||
// Our previous initialization code
|
||||
window.onpopstate = () => updateRoute();
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
init();
|
||||
```
|
||||
|
||||
Here we retrieve the saved data, and if there's any we update the state accordingly. It's important to do this *before* updating the route, as there might be code relying on the state during the page update.
|
||||
|
||||
We can also make the *Dashboard* page our application default page, as we are now persisting the account data. If no data is found, the dashboard takes care of redirecting to the *Login* page anyways. In `updateRoute()`, replace the fallback `return navigate('/login');` with `return navigate('/dashboard');`.
|
||||
|
||||
Now login in the app and try refreshing the page. You should stay on the dashboard. With that update we've taken care of all our initial issues...
|
||||
|
||||
## Refresh the data
|
||||
|
||||
...But we might also have a created a new one. Oops!
|
||||
|
||||
Go to the dashboard using the `test` account, then run this command on a terminal to create a new transaction:
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
|
||||
http://localhost:5000/api/accounts/test/transactions
|
||||
```
|
||||
|
||||
Try refreshing your the dashboard page in the browser now. What happens? Do you see the new transaction?
|
||||
|
||||
The state is persisted indefinitely thanks to the `localStorage`, but that also means it's never updated until you log out of the app and log in again!
|
||||
|
||||
One possible strategy to fix that is to reload the account data every time the dashboard is loaded, to avoid stall data.
|
||||
|
||||
### Task
|
||||
|
||||
Create a new function `updateAccountData`:
|
||||
|
||||
```js
|
||||
async function updateAccountData() {
|
||||
const account = state.account;
|
||||
if (!account) {
|
||||
return logout();
|
||||
}
|
||||
|
||||
const data = await getAccount(account.user);
|
||||
if (data.error) {
|
||||
return logout();
|
||||
}
|
||||
|
||||
updateState('account', data);
|
||||
}
|
||||
```
|
||||
|
||||
This method checks that we are currently logged in then reloads the account data from the server.
|
||||
|
||||
Create another function named `refresh`:
|
||||
|
||||
```js
|
||||
async function refresh() {
|
||||
await updateAccountData();
|
||||
updateDashboard();
|
||||
}
|
||||
```
|
||||
|
||||
This one updates the account data, then takes care of updating the HTML of the dashboard page. It's what we need to call when the dashboard route is loaded. Update the route definition with:
|
||||
|
||||
```js
|
||||
const routes = {
|
||||
'/login': { templateId: 'login' },
|
||||
'/dashboard': { templateId: 'dashboard', init: refresh }
|
||||
};
|
||||
```
|
||||
|
||||
Try reloading the dashboard now, it should display the updated account data.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Challenge
|
||||
|
||||
Now that we reload the account data every time the dashboard is loaded, do you think we still need to persist *all the account* data?
|
||||
|
||||
Try working together to change what is saved and loaded from `localStorage` to only include what is absolutely required for the app to work.
|
||||
|
||||
## Post-Lecture Quiz
|
||||
|
||||
[Post-lecture quiz](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/48)
|
||||
|
||||
## Assignment
|
||||
|
||||
[Implement "Add transaction" dialog](assignment.md)
|
||||
|
||||
Here's an example result after completing the assignment:
|
||||
|
||||

|
Reference in New Issue
Block a user