Categories
JavaScript Nodejs

Use JSON Web Tokens to Make a Secure Web App

With single page front end apps and mobile apps being more popular than ever, the front end is decoupled from the back end. Since almost all web apps need authentication, there needs to be a way for front end or mobile apps to store user identity data in a secure fashion.

JSON Web Tokens (JWT) is one of the most common ways to store authentication data on front end apps. With Node.js there are popular libraries that can generate and verify the JWT by check for its authenticity by check against a secret key stored in the back end and also check for expiry date.

The token is encoded in a standard format that is understood by most apps. It usually contains user identity data like user ID, user name, etc. It is given to the user when the user can successfully complete authentication.

In this piece, we will build an app that uses JWT to store authentication data. On the back end, we will use the Express framework, which runs on Node.js, and the jsonwebtoken package for generating and verify the token. For the front end, we will use the Angular framework and the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key and returned to the user, and stored on the front end app in local storage. Whenever the user needs to access authenticated routes on the back end, they will need the token. There will be a function in the back end app called middleware to check for a valid token. A valid token is one that is not expired and verifies to be valid against our secret key. There will also be signed up, and user credential settings pages in addition to the login page.


Now that we have our plan, we can begin by creating the front and back end app folders. Make one for each. Then we start writing the back end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we need to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors . @babel/register allows us to use the latest JavaScript features. express-jwt generates the JWT and verifies it against a secret.bcrypt does the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests.

Then we make our database migrations. Run npx sequelize-cli init to generate the skeleton code for our database to object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string

We make another migration and put:

'use strict';

module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.addConstraint(  
        "Users",  
        ["email"],  
        {  
          type: "unique",  
          name: 'emailUnique'  
        }), 
      queryInterface.addConstraint(  
        "Users",  
        ["userName"],  
        {  
          type: "unique",  
          name: 'userNameUnique'  
        }),  
  }, 

  down: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.removeConstraint(  
        "Users",  
        'emailUnique'  
      ), 
      queryInterface.removeConstraint(  
        "Users",  
        'userNameUnique'  
      ),  
    ])  
  }  
};

This makes sure we don’t have duplicate entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Let’s write some code. Put the following in app.js:

require("babel/register");  
require("babel-polyfill");  
require('dotenv').config();  
const express = require('express');  
const bodyParser = require('body-parser');  
const cors = require('cors');  
const user = require('./controllers/userController');  
const app = express();
app.use(cors())  
app.use(bodyParser.urlencoded({ extended: true }));  
app.use(bodyParser.json());

app.use((req, res, next) => {  
  res.locals.session = req.session;  
  next();  
});  
  
app.use('/user', user);

app.get('*', (req, res) => {  
  res.redirect('/home');  
});

app.listen((process.env.PORT || 8080), () => {  
  console.log('App running on port 8080!');  
});

We need:

require("babel/register");  
require("babel-polyfill");

to use the latest features in JavaScript.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in controllers folder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

const express = require('express');  
const bcrypt = require('bcrypt');  
const router = express.Router();  
const models = require('../models');  
const jwt = require('jsonwebtoken');  
import { authCheck } from '../middlewares/authCheck';

router.post('/login', async (req, res) => {  
    const secret = process.env.JWT_SECRET;  
    const userName = req.body.userName;  
    const password = req.body.password;  
    if (!userName || !password) {  
        return res.send({  
            error: 'User name and password required'  
        })  
    }  
    const users = await models.User.findAll({  
        where: {  
            userName  
        }  
    }) 

    const user = users[0];  
    if (!user) {  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    } 

    try {  
        const compareRes = await bcrypt.compare(password, user.hashedPassword);  
        if (compareRes) {  
            const token = jwt.sign(  
                {  
                    data: {  
                        userName,  
                        userId: user.id  
                    }  
                },  
                secret,  
                { expiresIn: 60 * 60 }  
            );  
            return res.send({ token });  
        }  
        else {  
            res.status(401);  
            return res.send({  
                error: 'Invalid username or password'  
            });  
        }  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    }});

router.post('/signup', async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const password = req.body.password;  
    try {  
        const hashedPassword = await bcrypt.hash(password, 10)  
        await models.User.create({  
            userName,  
            email,  
            hashedPassword  
        })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }  
});

router.put('/updateUser', authCheck, async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const token = req.headers.authorization;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        await models.User.update({  
            userName,  
            email  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

router.put('/updatePassword', authCheck, async (req, res) => {  
    const token = req.headers.authorization;  
    const password = req.body.password;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        const hashedPassword = await bcrypt.hash(password, saltRounds)  
        await models.User.update({  
            hashedPassword  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

module.exports = router;

The login route searches for the User entry, then, if it’s found, checks for the hashed password with the compare function of bcrypt. If both are successful, a JWT is generated. The signup route gets JSON payload of username and password and saves it. Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text. bcrypt.hash takes two arguments. This first is the plain text password, and the second is the number of salt rounds.

updatePassword route is an authenticated route. It checks for the token and, if it’s valid, will continue to save the user’s password by searching for the User with the user ID that matches the decoded token. We will add the authCheck middleware next.

Create a middlewares folder and create authCheck.js inside it.

const jwt = require('jsonwebtoken');  
const secret = process.env.JWT_SECRET;export const authCheck = (req, res, next) => {  
    if (req.headers.authorization) {  
        const token = req.headers.authorization;  
        jwt.verify(token, secret, (err, decoded) => {  
            if (err) {  
                res.send(401);  
            }  
            else {  
                next();  
            }  
        });  
    }  
    else {  
        res.send(401);  
    }  
}

This allows us to check for authentication in authenticated routes without repeating code. We place if in between the URL and our main route code in each authenticated route by importing this and referencing it.

We make an .env file of the root of the back end app folder, with this content:

DB_HOST='localhost'  
DB_NAME='login-app'  
DB_USERNAME='db-username'  
DB_PASSWORD='db-password'  
JWT_SECRET='secret'

The back end app is now complete. Now we move on to the front end Angular app.

Switch to your front end app folder. To build the Angular app, you need the Angular CLI.

To install it, run npm i -g @angular/cli in your Node.js command prompt. Then, run ng new frontend to generate the skeleton code for your front end app.

Also, install @angular/material according to the Angular documentation.

After that, replace the default app.module.ts with the following:

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import {  
  MatButtonModule,  
  MatCheckboxModule,  
  MatInputModule,  
  MatMenuModule,  
  MatSidenavModule,  
  MatToolbarModule,  
  MatTableModule,  
  MatDialogModule,  
  MAT_DIALOG_DEFAULT_OPTIONS,  
  MatDatepickerModule,  
  MatSelectModule,  
  MatCardModule  
} from '@angular/material';  
import { MatFormFieldModule } from '[@angular/material](http://twitter.com/angular/material)/form-field';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { FormsModule } from '@angular/forms';  
import { TopBarComponent } from './top-bar/top-bar.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
import { SessionService } from './session.service';  
import { HttpReqInterceptor } from './http-req-interceptor';  
import { UserService } from './user.service';  
import { CapitalizePipe } from './capitalize.pipe';

@NgModule({  
  declarations: [  
    AppComponent,  
    TopBarComponent,  
    HomePageComponent,  
    LoginPageComponent,  
    SignUpPageComponent,  
    SettingsPageComponent,  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatCheckboxModule,  
    MatFormFieldModule,  
    MatInputModule,  
    MatMenuModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatTableModule,  
    FormsModule,  
    HttpClientModule,  
    MatDialogModule,  
    MatDatepickerModule,  
    MatMomentDateModule,  
    MatSelectModule,  
    MatCardModule,  
    NgxMaterialTimepickerModule  
  ],  
  providers: [  
    SessionService,  
    {  
      provide: HTTP_INTERCEPTORS,  
      useClass: HttpReqInterceptor,  
      multi: true  
    },  
    UserService,  
    {  
      provide: MAT_DIALOG_DEFAULT_OPTIONS,  
      useValue: { hasBackdrop: false }  
    },  
  ],  
  bootstrap: [AppComponent],  
})  
export class AppModule { }

This creates all the dependencies and components which we will add. In order to make authenticated requests easy with our token, we create an HTTP request interceptor by creating http-req-interceptor.ts:

import { Injectable } from '@angular/core';  
import {  
    HttpEvent,  
    HttpInterceptor,  
    HttpHandler,  
    HttpResponse,  
    HttpErrorResponse,  
    HttpRequest  
} from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { environment } from '../environments/environment'  
import { map, filter, tap } from 'rxjs/operators';  
import { Router } from '@angular/router';

@Injectable()  
export class HttpReqInterceptor implements HttpInterceptor {  
    constructor(  
        public router: Router  
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        let modifiedReq = req.clone({}); 
        if (localStorage.getItem('token')) {  
            modifiedReq = modifiedReq.clone({  
                setHeaders: {  
                    authorization: localStorage.getItem('token')  
                }  
            });  
        } 
        return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {  
            if (event instanceof HttpResponse) {}  
        });  
    }  
}

We set the token in all requests except the login request.

In our environments/environment.ts, we have:

export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:8080'
};

This points to our back end’s URL.

Now we need to make our side nav. We want to add @ngrx/store to store the side nav’s state. We install the package by running npm install @ngrx/store --save. We add our reducer by running ng add @ngrx/store to add our reducers.

We add menu-reducers.ts to set the state centrally in our flux store:

const TOGGLE_MENU = 'TOGGLE_MENU';function menuReducer(state, action) {  
    switch (action.type) {  
        case TOGGLE_MENU:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { menuReducer, TOGGLE_MENU };

To link our reducer to other parts of the app, put the following in index.ts:

import { menuReducer } from './menu-reducer';  
import { tweetsReducer } from './tweets-reducer';export const reducers = {  
  menu: menuReducer,  
};

To get our Material Design look, add the following to style.css:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";  
body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

Between the head tags in index.html, we add:

<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Then we add a service for the user functions, by running ng g service user . The will create user.service.ts . We then put:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';  
import { Router } from '[@angular/router](http://twitter.com/angular/router)';  
import { JwtHelperService } from "@auth0/angular-jwt";

const helper = new JwtHelperService();

@Injectable({  
  providedIn: 'root'  
})  
export class UserService { constructor(  
    private http: HttpClient,  
    private router: Router  
  ) { } 

  signUp(data) {  
    return this.http.post(`${environment.apiUrl}/user/signup`, data);  
  } 

  updateUser(data) {  
    return this.http.put(`${environment.apiUrl}/user/updateUser`, data);  
  } 

  updatePassword(data) {  
    return this.http.put(`${environment.apiUrl}/user/updatePassword`, data);  
  } 

  login(data) {  
    return this.http.post(`${environment.apiUrl}/user/login`, data);  
  } 

  logOut() {  
    localStorage.clear();  
    this.router.navigate(['/']);  
  } 

  isAuthenticated() {  
    try {  
      const token = localStorage.getItem('token');  
      const decodedToken = helper.decodeToken(token);  
      const isExpired = helper.isTokenExpired(token);  
      return !!decodedToken && !isExpired;  
    }  
    catch (ex) {  
      return false;  
    }  
  }}

Each function requests a subscription for HTTP request except for the isAuthenticated function which is used to check for the token’s validity.

We also need routing for our app so we can see the pages when we go to the URLs listed below. In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { TweetsPageComponent } from './tweets-page/tweets-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component';  
import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component';  
import { IsAuthenticatedGuard } from './is-authenticated.guard';const routes: Routes = [  
  { path: 'login', component: LoginPageComponent },  
  { path: 'signup', component: SignUpPageComponent },  
  { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] },  
  { path: '**', component: HomePageComponent }];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

Now we create the parts that are referenced in the file above. We need to prevent people from access authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated . This generates is-authenticated.guard.ts.

We put the following in is-authenticated.guard.ts :

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';  
import { Observable } from 'rxjs';  
import { UserService } from './user.service';

@Injectable({  
  providedIn: 'root'  
})  
export class IsAuthenticatedGuard implements CanActivate {  
  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    const isAuthenticated = this.userService.isAuthenticated();  
    if (!isAuthenticated) {  
      localStorage.clear();  
      this.router.navigate(['/']);  
    }  
    return isAuthenticated;  
  }}

This uses our isAuthenticated function from UserService to check for a valid token. If it’s not valid, we clear it and redirect it back to the home page.

Now we create the forms for logging in setting the user data after logging. We run ng g component homePage, ng g component loginPage, ng g component topBar, ng g component signUpPage, and ng g component settingsPage . These are for the forms and the top bar components.

The home page is just a static page. We should have home-page.component.html and home-page.component.tsgenerated after running the commands in our last paragraph.

In home-page.component.html we put:

<div class="center">  
    <h1>Home Page</h1>  
</div>

Now we make our login page. In login-page.component.ts, we put:

<div class="center">  
    <h1>Log In</h1>  
</div>  
<form #loginForm='ngForm' (ngSubmit)='login(loginForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='loginData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='loginData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Log In</button>  
    <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a>  
</form>

In login-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';
[@Component](http://twitter.com/Component)({  
  selector: 'app-login-page',  
  templateUrl: './login-page.component.html',  
  styleUrls: ['./login-page.component.scss']  
})  
export class LoginPageComponent implements OnInit {  
  loginData: any = <any>{}; 

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { } 

  ngOnInit() {  
  } 

  login(loginForm: NgForm) {  
    if (loginForm.invalid) {  
      return;  
    }  
    this.userService.login(this.loginData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/settings']);  
      }, err => {  
        alert('Invalid username or password');  
      })  
  }  
}

We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise, an error alert will be displayed.

Then in our sign up page, sign-up-page.component.html, we put:

<div class="center">  
    <h1>Sign Up</h1>  
</div>  
<br>  
<form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='signUpData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='signUpData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='signUpData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Sign Up</button>  
</form>

and in sign-up-page.component.ts, we put:

import { Component, OnInit } from '[@angular/core](http://twitter.com/angular/core)';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';  
import _ from 'lodash';

@Component({  
  selector: 'app-sign-up-page',  
  templateUrl: './sign-up-page.component.html',  
  styleUrls: ['./sign-up-page.component.scss']  
})  
export class SignUpPageComponent implements OnInit {  
  signUpData: any = <any>{}; constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  } 

  signUp(signUpForm: NgForm) {  
    if (signUpForm.invalid) {  
      return;  
    }  
    this.userService.signUp(this.signUpData)  
      .subscribe(res => {  
        this.login();  
      }, err => {  
        console.log(err);  
        if (  
          _.has(err, 'error.error.errors') &&  
          Array.isArray(err.error.error.errors) &&  
          err.error.error.errors.length > 0  
        ) {  
          alert(err.error.error.errors[0].message);  
        }  
      })  
  } 

  login() {  
    this.userService.login(this.signUpData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/tweets']);  
      })  
  }  
}

These two pieces of code get the sign-up data and send it to the back end, which will save the file if they are all valid.

Similarly, in the settings-page.component.html:

<div class="center">  
    <h1>Settings</h1>  
</div>  
<br>  
<div>  
    <h2>Update User Info</h2>  
</div>  
<br>  
<form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='updateUserData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='updateUserData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update User Info</button>  
</form>  
<br><div>  
    <h2>Update Password</h2>  
</div>  
<br>  
<form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='updatePasswordData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update Password</button>  
</form>  
<br>
<div *ngIf='currentTwitterUser.id' class="title">  
    <h2>Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button>  
    </div>  
</div>  
<div *ngIf='!currentTwitterUser.id' class="title">  
    <h2>Not Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button>  
    </div>  
</div>

In settings-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { ActivatedRoute, Router } from '@angular/router';  
import { SessionService } from '../session.service';  
import { NgForm } from '@angular/forms';  
import { UserService } from '../user.service';

@Component({  
  selector: 'app-settings-page',  
  templateUrl: './settings-page.component.html',  
  styleUrls: ['./settings-page.component.scss']  
})  
export class SettingsPageComponent implements OnInit {  
  currentTwitterUser: any = <any>{};  
  elements: any[] = [];  
  displayedColumns: string[] = ['key', 'value'];  
  updateUserData: any = <any>{};  
  updatePasswordData: any = <any>{}; constructor(  
    private sessionService: SessionService,  
    private userService: UserService,  
    private router: Router  
  ) {  
  
  } 

  ngOnInit() {  
  
  } 

  updateUser(updateUserForm: NgForm) {  
    if (updateUserForm.invalid) {  
      return;  
    }  
    this.userService.updateUser(this.updateUserData)  
      .subscribe(res => {  
        alert('Updated user info successful.');  
      }, err => {  
        alert('Updated user info failed.');  
      })  
  } 

  updatePassword(updatePasswordForm: NgForm) {  
    if (updatePasswordForm.invalid) {  
      return;  
    }  
    this.userService.updatePassword(this.updatePasswordData)  
      .subscribe(res => {  
        alert('Updated password successful.');  
      }, err => {  
        alert('Updated password failed.');  
      })  
  }  
}

Similar to other pages, this sends a request payload for changing user data and password to our back end.

Finally, to make our top bar, we put the following in top-bar.component.html:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Twitter Automator  
</mat-toolbar>

And in top-bar.component.ts:

import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from '../reducers/menu-reducer';

@Component({  
  selector: 'app-top-bar',  
  templateUrl: './top-bar.component.html',  
  styleUrls: ['./top-bar.component.scss']  
})  
export class TopBarComponent implements OnInit {  
  menuOpen: boolean; constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  ngOnInit() {  
  } 

  toggleMenu() {  
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });  
  }  
}

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from './reducers/menu-reducer';  
import { UserService } from './user.service';

@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean; 

  constructor(  
    private store: Store<any>,  
    private userService: UserService  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  isAuthenticated() {  
    return this.userService.isAuthenticated();  
  }

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

  logOut() {  
    this.userService.logOut();  
  }  
}

and in app.component.html, we have:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    Twitter Automator  
                </b>  
            </li>  
            <li>  
                <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a>  
            </li>  
            <li>  
                <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a>  
            </li>  
            <li>  
                <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a>  
            </li>  
            <li>  
                <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a>  
            </li>  
            <li>  
                <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a>  
            </li>  
        </ul>
    </mat-sidenav>  
    <mat-sidenav-content>  
        <app-top-bar></app-top-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This allows us to toggle our side navigation menu. Note that we have:

@HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

to detect clicks out the side nav. If we click outside, i.e. not clicking on any element with those classes, then we close the menu. this.store.dispatch propagates the closed state to all components.

Categories
JavaScript Nodejs

Use GraphQL APIs in Vue.js Apps

GraphQL is a query language made by Facebook for sending requests over the internet. It uses its own query but still sends data over HTTP. It uses one endpoint only for sending data.

The benefits of using GraphQL include being able to specify data types for the data fields you are sending and being able to specify the types of data fields that are returned.

The syntax is easy to understand, and it is simple. The data are still returned in JSON for easy access and manipulation. This is why GraphQL has been gaining traction in recent years.

GraphQL requests are still HTTP requests. However, you are always sending and getting data over one endpoint. Usually, this is the graphql endpoint. All requests are POST requests, no matter if you are getting, manipulating, or deleting data.

To distinguish between getting and manipulating data, GraphQL requests can be classified as queries and mutations. Below is one example of a GraphQL request:

{  
  getPhotos(page: 1) {  
    photos {  
      id  
      fileLocation  
      description  
      tags  
    }  
    page  
    totalPhotos  
  }  
}

In this story, we will build a Vue.js app that uses the GraphQL Jobs API located at https://graphql.jobs /to display jobs data. To start building the app, we first install the Vue CLI by running npm i @vue/cli. We need the latest version of Node.js LTS installed. After that, we run vue create jobs-app to create new Vue.js project files for our app.

Then, we install some libraries we need for our app, which include a GraphQL client, Vue Material, and VeeValidate for form validation. We run:

npm i vue-apollo vue-material vee-validate@2.2.14 graphql-tag

This installs the packages. Vue Apollo is the GraphQL client, and graphQL-tag converts GraphQL query strings into queries that are usable by Vue Apollo.

Next, we are ready to write some code. First, we write some helper code for our components. We add a mixin for making the GraphQL queries to the Jobs API. Create a new folder called mixins, and add a file called jobMixins.js to it. Then in the file, we add:

import { gql } from "apollo-boost";

export const jobsMixin = {  
  methods: {  
    getJobs(type) {  
      const getJobs = gql`  
      query jobs(  
          $input: JobsInput,  
        ){  
          jobs(  
            input: $input  
          ) {  
            id,  
            title,  
            slug,  
            commitment {  
              id,  
              title,  
              slug  
            },  
            cities {  
              name  
            },  
            countries {  
              name  
            },  
            remotes {  
              name  
            },  
            description,  
            applyUrl,  
            company {  
              name  
            }  
          }  
        }  
      `;  
      return this.$apollo.query({  
        query: getJobs,  
        variables: {  
          type  
        }  
      });  
    }, 

    getCompanies() {  
      const getCompanies = gql`  
      query companies{  
          companies {  
            id,  
            name,  
            slug,  
            websiteUrl,  
            logoUrl,  
            twitter,  
            jobs {  
              id,  
              title  
            }  
          }  
        }  
      `;  
      return this.$apollo.query({  
        query: getCompanies  
      });  
    }  
  }  
}

These functions will get the data we require from the GraphQL Jobs API. The gql in front of the string is a tag. A tag is an expression, which is usually a function that is run to map a string into something else.

In this case, it will map the GraphQL query string into a query object that can be used by the Apollo client.

this.$apollo is provided by the Vue Apollo library. It is available since we will include it in main.js.

Next, in the view folder, we create a file called Companies.vue, and we add:

<template>  
  <div class="home">  
    <div class="center">  
      <h1>Companies</h1>  
    </div>  
    <md-card md-with-hover v-for="c in companies" :key="c.id">  
      <md-card-header>  
        <div class="md-title">  
          <img :src="c.logoUrl" class="logo" />  
          {{c.name}}  
        </div>  
        <div class="md-subhead">  
          <a :href="c.websiteUrl">Link</a>  
        </div>  
        <div class="md-subhead">Twitter: {{c.twitter}}</div>  
      </md-card-header><md-card-content>  
        <md-list>  
          <md-list-item>  
            <h2>Jobs</h2>  
          </md-list-item>  
          <md-list-item v-for="j in c.jobs" :key="j.id">{{j.title}}</md-list-item>  
        </md-list>  
      </md-card-content>  
    </md-card>  
  </div>  
</template>

<script>  
import { jobsMixin } from "../mixins/jobsMixin";  
import { photosUrl } from "../helpers/exports";

export default {  
  name: "home",  
  mixins: [jobsMixin],  
  computed: {  
    isFormDirty() {  
      return Object.keys(this.fields).some(key => this.fields[key].dirty);  
    }  
  },  
  async beforeMount() {  
    const response = await this.getCompanies();  
    this.companies = response.data.companies;  
  },  
  data() {  
    return {  
      companies: []  
    };  
  },  
  methods: {}  
};  
</script>

<style lang="scss" scoped>  
.logo {  
  width: 20px;  
}

.md-card-header {  
  padding: 5px 34px;  
}  
</style>

It uses the mixin function that we created to get the companies’ data and displays it to the user.

In Home.vue, we replace the existing code with the following:

<template>  
  <div class="home">  
    <div class="center">  
      <h1>Home</h1>  
    </div>  
    <form @submit="search" novalidate>  
      <md-field :class="{ 'md-invalid': errors.has('term') }">  
        <label for="term">Search</label>  
        <md-input type="text" name="term" v-model="searchData.type" v-validate="'required'"></md-input>  
        <span class="md-error" v-if="errors.has('term')">{{errors.first('term')}}</span>  
      </md-field> <md-button class="md-raised" type="submit">Search</md-button>  
    </form>  
    <br />  
    <md-card md-with-hover v-for="j in jobs" :key="j.id">  
      <md-card-header>  
        <div class="md-title">{{j.title}}</div>  
        <div class="md-subhead">{{j.company.name}}</div>  
        <div class="md-subhead">{{j.commitment.title}}</div>  
        <div class="md-subhead">Cities: {{j.cities.map(c=>c.name).join(', ')}}</div>  
      </md-card-header> <md-card-content>  
        <p>{{j.description}}</p>  
      </md-card-content><md-card-actions>  
        <md-button v-on:click.stop.prevent="goTo(j.applyUrl)">Apply</md-button>  
      </md-card-actions>  
    </md-card>  
  </div>  
</template>

<script>  
import { jobsMixin } from "../mixins/jobsMixin";  
import { photosUrl } from "../helpers/exports";export default {  
  name: "home",  
  mixins: [jobsMixin],  
  computed: {  
    isFormDirty() {  
      return Object.keys(this.fields).some(key => this.fields[key].dirty);  
    }  
  },  
  beforeMount() {},  
  data() {  
    return {  
      searchData: {  
        type: ""  
      },  
      jobs: []  
    };  
  },  
  methods: {  
    async search(evt) {  
      evt.preventDefault();  
      if (!this.isFormDirty || this.errors.items.length > 0) {  
        return;  
      }  
      const { type } = this.searchData;  
      const response = await this.getJobs(this.searchData.type);  
      this.jobs = response.data.jobs;  
    }, goTo(url) {  
      window.open(url, "_blank");  
    }  
  }  
};  
</script>

<style lang="scss">  
.md-card-header {  
  .md-title {  
    color: black !important;  
  }  
}

.md-card {  
  width: 95vw;  
  margin: 0 auto;  
}  
</style>

In the code above, we have a search form to let users search for jobs with the keyword they entered. The results are displayed in the card.

In App.vue, we replace the existing code with the following:

<template>  
  <div id="app">  
    <md-toolbar>  
      <md-button class="md-icon-button" @click="showNavigation = true">  
        <md-icon>menu</md-icon>  
      </md-button>  
      <h3 class="md-title">GraphQL Jobs App</h3>  
    </md-toolbar>  
    <md-drawer :md-active.sync="showNavigation" md-swipeable>  
      <md-toolbar class="md-transparent" md-elevation="0">  
        <span class="md-title">GraphQL Jobs App</span>  
      </md-toolbar><md-list>  
        <md-list-item>  
          <router-link to="/">  
            <span class="md-list-item-text">Home</span>  
          </router-link>  
        </md-list-item><md-list-item>  
          <router-link to="/companies">  
            <span class="md-list-item-text">Companies</span>  
          </router-link>  
        </md-list-item>  
      </md-list>  
    </md-drawer><router-view />  
  </div>  
</template>

<script>  
export default {  
  name: "app",  
  data: () => {  
    return {  
      showNavigation: false  
    };  
  }  
};  
</script>

<style lang="scss">  
.center {  
  text-align: center;  
}

form {  
  width: 95vw;  
  margin: 0 auto;  
}

.md-toolbar.md-theme-default {  
  background: #009688 !important;  
  height: 60px;  
}

.md-title,  
.md-toolbar.md-theme-default .md-icon {  
  color: #fff !important;  
}  
</style>

This adds a top bar and left menu to our app and allows us to toggle the menu. It also allows us to display the pages we created in the router-view element.

In main.js, we put:

import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
import VueMaterial from 'vue-material';  
import VeeValidate from 'vee-validate';  
import 'vue-material/dist/vue-material.min.css'  
import 'vue-material/dist/theme/default.css'  
import VueApollo from 'vue-apollo';  
import ApolloClient from 'apollo-boost';

Vue.config.productionTip = false  
Vue.use(VeeValidate);  
Vue.use(VueMaterial);  
Vue.use(VueApollo);const client = new ApolloClient({  
  uri: '[https://api.graphql.jobs'](https://api.graphql.jobs'),  
  request: operation => {  
    operation.setContext({  
      headers: {  
        authorization: ''  
      },  
    });  
  }  
});\

const apolloProvider = new VueApollo({  
  defaultClient: client,  
})

new Vue({  
  router,  
  store,  
  apolloProvider,  
  render: h => h(App)  
}).$mount('#app')

This adds the libraries we use in the app (such as Vue Material) and adds the Apollo Client to our app so we can use them in our app.

The this.$apollo object is available in our components and mixins because we inserted apolloProvider in the object we use in the argument of new Vue.

In router.js, we put:

import Vue from 'vue'  
import Router from 'vue-router'  
import Home from './views/Home.vue'  
import Companies from './views/Companies.vue'Vue.use(Router)export default new Router({  
  mode: 'history',  
  base: process.env.BASE_URL,  
  routes: [  
    {  
      path: '/',  
      name: 'home',  
      component: Home  
    },  
    {  
      path: '/companies',  
      name: 'companies',  
      component: Companies  
    }  
  ]  
})

Now we can see the pages we created when we navigate to them.

Categories
JavaScript JavaScript Basics

Using the JavaScript Number Object

The Number JavaScript object is a wrapper object that lets us work with numbers by providing us with various constants and methods. A primitive number can be created by the Number() function. A JavaScript number is a double-precision 64-bit binary format IEEE 754 value.


Creating a Number Object

We create a Number object using the Number function by writing the following code:

new Number('123');   
const a = new Number('123');  
const b = Number('123');

The main purpose of the Number function is to create a number from a string. The Number constructor used with the new operator is for creating a Number object, which is of type “object” instead of “number.” A number primitive isn’t an instance of Number. The data type for a primitive number value is a number. The type with the typeof operator and the constructor that it’s constructed from with the instanceof operator, are as in the following code:

console.log(typeof new Number('123'));  
console.log(new Number('123') instanceof Number);

We get “object” and true respectively when we run the two lines of code above. On the other hand, if we write this code instead:

console.log(typeof Number('123'));  
console.log(Number('123') instanceof Number);

We get number and false respectively.

The primary uses of the Number object are for checking if an object can be converted to a number. If a non-number is passed into the Number function, we get NaN returned. If the Number is used without the new operator in a non-constructor context, it’s handy to use for type conversion.

The Number object has the following properties:

  • Number.EPSILON: the smallest interval between two representable numbers.
  • Number.MAX_SAFE_INTEGER: the maximum safe integer in JavaScript, 2⁵³ minus 1.
  • Number.MAX_VALUE: the largest positive representable number (1.7976931348623157e+308).
  • Number.MIN_SAFE_INTEGER: the minimum safe integer in JavaScript, -2⁵³ minus 1.
  • Number.MIN_VALUE: the smallest positive representable number; the positive number closest to zero without actually being zero.
  • Number.NaN: the "not a number" value.
  • Number.NEGATIVE_INFINITY: value representing negative infinity.
  • Number.POSITIVE_INFINITY: value representing infinity.

Static Methods

The Number object has a few static methods.

Number.isNaN()

Determines whether the passed value is NaN. Returns true if a variable’s value is NaN. For example, we can use it to determine if an object is a number with the following code:

console.log(Number.isNaN(+'abc')); // true  
console.log(Number.isNaN(+'123')); // false

The first line logs true because when 'abc' is converted to a number with the unary + operator, we get NaN. On the other hand, the second line logs false because when '123' is converted to a number with a + operator, we get 123, which is a number, so the second line logs false.

Number.isFinite()

Determines whether the passed value is a finite number. Returns true if a number is finite and false otherwise. For example, we can use it to determine if an object is a number with the following code:

console.log(Number.isFinite(Infinity)); // false  
console.log(Number.isFinite(123)); // true

We get false for the first line because Infinity isn’t a finite number, but the second line logs true because 123 is a finite number.

Number.isInteger()

Determines whether the passed value is an integer. Returns true if the number is an integer and false otherwise. For example, we can use this method as in the following code:

console.log(Number.isInteger(123)); // true  
console.log(Number.isInteger(123.45)); // false

The first line logs true because 123 is an integer, but the second line logs false because it’s not an integer. If the argument passed is not of the number type then it will return false. For example, Number.isInteger('10'); will return false.

Number.isSafeInteger()

Determines whether an integer passed in the argument is within the range of a safe integer, i.e. if the number is between -2⁵³ minus 1 and 2⁵³ minus 1. For example, if we have:

console.log(Number.isSafeInteger(Math.pow(2, 53) - 1)); // true  
console.log(Number.isSafeInteger(Math.pow(2, 53))); // false

The first line logs true because Math.pow(2, 53) — 1 is within the safe range, but the second line logs false because Math.pow(2, 53) is not in the safe range.

Number.parseFloat()

The parseFloat method converts a string that’s passed into the argument and returns a number that’s parsed from the string as a floating-point number. If it can’t be parsed, then NaN is returned. For example, if we have:

console.log(Number.parseFloat('123.45'));  
console.log(Number.parseFloat('123'));  
console.log(Number.parseFloat('abc'));

We get 123.45 from the first line, 123 from the second line, and NaN from the third line.

Number.parseInt()

The parseInt method converts a string that’s passed into the argument and returns a number that’s parsed from the string as a whole number. If the first character of the string can’t be parsed, and the radix is smaller than 11, then NaN is returned. For example, if we have:

console.log(Number.parseFloat('123.45'));  
console.log(Number.parseFloat('123'));  
console.log(Number.parseFloat('abc'));

Then we get 123 from the first line and second line, and NaN from the third line.

It also takes a radix as the second argument, which is the base of the mathematical numeral systems. If the string starts with 0x then the radix will be set to 16. If the string starts with anything else, then the radix will be set to 10.

To convert a hexadecimal string into a number, we can write something like the following:

console.log(Number.parseInt('0x1'));

We get 1 when the last line is run. To convert a binary string to decimal, we can write something like:

console.log(Number.parseInt('0111', 2));

The line above will log 7, which is the decimal equivalent of the binary number 0111.


<img class="do t u hm ak" src="https://miro.medium.com/max/8000/0*azxvdPlJ5DchQYGA" width="4000" height="2423" role="presentation"/>

Photo by Andrew Buchanan on Unsplash

Instance Methods

All Number instances inherit from Number.prototype, which provides the object with a few instance methods. The instance methods for Number follow.

Number.toExponential()

The toExponential method returns a string representing the number in exponential notation. It takes an optional that specifies the number of fractional digits to include. For example, we can write:

(123).toExponential(1);

Then we get 1.2e+2.

Number.toFixed()

The toFixed method returns a string representing the number in fixed-point notation. It takes an optional that specifies the number of fractional digits to include after the decimal point. For example, we can write:

console.log((123).toFixed(1));

And we get 123.0.

Number.toLocaleString()

This method returns a string with a language-sensitive representation of the number. It overrides the Object.prototype.toLocaleString() method. The first argument is the locales argument, which takes one locale string or an array of locale strings. This is an optional argument. It takes a BCP 47 language tag with the optional Unicode extension key nu to specify the numbering system for formatting the number. Possible values for nu include: "arab", "arabext", "bali", "beng", "deva", "fullwide", "gujr", "guru", "hanidec", "khmr", "knda", "laoo", "latn", "limb", "mlym", "mong", "mymr", "orya", "tamldec", "telu", "thai", "tibt". The instance of the object created by the constructor has the format method return a string with the formatted number.

The second argument accepts an object with a few properties: localeMatcher, style, unitDisplay, currency, useGrouping, minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, minimumSignificantDigits, and maximumSignificantDigits.

The localeMatcher option specifies the locale-matching algorithm to use. The possible values are lookup and best fit. The lookup algorithm searches for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least as, but possibly more-suited than the lookup algorithm.

Style

The style option specifies the formatting style to use. Possible values for the style option include decimal, currency, percent, and unit. decimal is the default option and it’s used for plain number formatting, currency is for currency formatting, percent is for percent formatting, and unit is for unit formatting.

Possible values for thecurrency property are ISO 4217 currency codes, such as USD for the U.S. dollar and EUR for Euro. There’s no default value. If the style property is set to currency then the currency property must be provided. The currencyDisplay property sets how the currency is displayed in currency formatting. Possible values are symbol for adding localized currency symbols and is the default value, code is for adding the ISO currency code, name to use a localized currency name such as “dollar.” useGrouping option is for setting the grouping separator to use for numbers. It’s a boolean value.

minimumIntegerDigits, minimumFractionDigits, and maximumFractionDigits are considered one group of options. minimumIntegerDigits specifies the minimum number of integer digits to use, ranging from 1 to 21, with 1 being the default option. minimumFractionDigits is the minimum number of fraction digits to use, ranging from 0 to 20. The default is 0 for plain number and percent formatting. The default for currency formatting is specified by the ISO 4217 currency code list, and 2 if it’s not specified in the list. maximumFractionDigits is the maximum number of fraction digits to use, with possible values ranging from 0 to 20. The default for plain numbers is the maximum between minimumFractionDigits and 3. The default for currency formatting is the maximum between minimumFractionDigits and the number of fractional unit digits provided by the ISO 4217 currency code list, or 2 if the list doesn’t provide that information. The default for percent formatting is the maximum between minimumFractionDigits and 0.

minimumSignificantDigits and maximumSignificantDigits are considered as another group of options. If at least one of the options in this group is defined, then the first group is ignored. minimumSignificantDigits is the minimum number of significant digits to use, with possible values ranging from 1 to 21 with the default being 1. maximumSignificantDigits is the maximum number of significant digits to use, with possible values ranging from 1 to 21, with the default being 21.

For example, we can use this method in the following examples:

const number = 123.45;console.log(number.toLocaleString('fr', {  
  style: 'currency',  
  currency: 'EUR'  
}));

console.log(number.toLocaleString('zh-Hant-CN-u-nu-hanidec', {  
  style: 'currency',  
  currency: 'JPY'  
}))

console.log(number.toLocaleString('en-IN', {  
  maximumSignificantDigits: 3  
}));

The first console.log statement will log 123,45 €, the second console.log will log ¥一二三, and the third console.log will get 123.

Number.toPrecision()

The toPrecision method returns a string representing the number to a specified precision in fixed-point or exponential notation. It takes an argument that optionally lets us specify the base between 1 and 100. If it’s not specified then all digits after the decimal point will be returned. For example, we can use it as in the following code:

const numObj = 5.12345;console.log(numObj.toPrecision());  
console.log(numObj.toPrecision(100));  
console.log(numObj.toPrecision(5));  
console.log(numObj.toPrecision(2));  
console.log(numObj.toPrecision(1));

We get the following logged for each line:

5.123455.1234500000000000596855898038484156131744384765625000000000000000000000000000000000000000000000000005.12355.15

Number.toString()

The toString method returns a string representing the specified object in the specified radix, or base of the number. It overrides the Object.prototype.toString() method. The radix is an optional argument that lets us specify the base between 2 and 36. The default radix is 10. If the number is negative the sign is preserved. If it’s not a whole number, then a dot sign will be used to separate the decimal places. For example, we can write:

const count = 10;  
  
console.log(count.toString());    // '10'  
console.log((15).toString());     // '15'  
console.log((15.2).toString());   // '15.2'

It also works for non-decimal numbers. For example, we can write:

console.log((-0b0111).toString(10)); // '-7'

It’s handy for converting numbers of different bases to other numbers.

Number.valueOf()

The valueOf method returns the primitive value of the specified object. It overrides the Object.prototype.valueOf() method. For example, if we have a Number object constructed with the new operator, we can write:

const numObj = new Number('10');  
const numPrim = numObj.valueOf();  
console.log(numPrim);  
console.log(typeof numPrim);

We get the following logged when the code above is run:

10  
number

Converting Other Objects to Numbers

With the Number function, we can convert Date objects and null to numbers. For example, for Date objects, we can write:

const d = new Date(2019, 0, 1, 0, 0, 0);  
console.log(Number(d));

We get 1546329600000, which is the UNIX timestamp for January 1, 2019. The null object will be converted to 0, so if we write:

console.log(Number(null));

We get 0.


Conclusion

The Number JavaScript object is a wrapper object that lets us work with numbers by providing us with various constants and methods. A primitive number can be created by the Number() function. It has a number of useful static and instance methods for checking numbers and converting between strings and numbers and vice versa. The toString method is also handy for converting numbers to different bases.

Categories
React

How to Add Form Validation to Your React App with Redux Form

We look at basic uses of Redux form.

Form validation is a frequently needed feature in web apps. React does not come with its own form validation since it’s supposed to be a view library that provides developers with code structure. Fortunately, developers have come up with many form validation solutions. One of them is Redux Form, located at https://redux-form.com/8.2.2/.

As its name suggests, Redux Form uses Redux for storing the form data. It requires its own reducer for storing a form’s data. The form component that you write connects to the built-in reducer to store the data. If you want to get initial data, you load the data in your Redux store, then Redux Form can retrieve it.

Form value checks and form validations errors are provided by you. You can get form values from the store when you need it. The form input components can be customized easily by passing our custom component into the Field component provided by Redux Form.

In this article, we will write an address book app to illustrate the use of Redux Form for form validation. We need will have a Redux store for storing the form data, the selected contact data when editing, and a list of contacts. To start, we will run Create React App by running npx create-react-app address-book to create the app.

After that is run, we add our own libraries. We need Axios for making HTTP requests, React Bootstrap for styling, React Redux and Redux for state management, React Router for routing, and of course, Redux Form for form validation.

We install them by running:

npm i axios react-bootstrap react-redux react-router-fom redux redux-form

With our libraries installed, we can start writing our app. We create all files in the src folder unless otherwise specified. To start, we create actionCreators.js in the src folder and add:

import { SET_CONTACTS, LOAD } from "./actions";

const setContacts = contacts => {
  return {
    type: SET_CONTACTS,
    payload: contacts
  };
};

const setCurrentContact = contact => {
  return {
    type: LOAD,
    data: contact
  };
};

export { setContacts, setCurrentContact };

These are the actions and payload that we dispatch to our Redux store. setContacts is for setting a list of contacts and setCurrentContact is for loading the contact for editing.

Next, we create actions.js and add:

const SET_CONTACTS = "SET_CONTACTS";
const LOAD = "LOAD";

export { SET_CONTACTS, LOAD };

These are the constants for the action types of our store dispatch actions. Next in App.js , we replace the existing code with:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>
  );
}

export default App;

to add the React Bootstrap Navbar component to the top of our page. We also add the React Router route for the home page that we’ll create.

Next, we create ContactForm.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import PropTypes from "prop-types";
import { addContact, editContact, getContacts } from "./requests";
import { connect } from "react-redux";
import { setContacts, setCurrentContact } from "./actionCreators";
import { Field, reduxForm } from "redux-form";
import { renderInputField } from "./RenderInputField";
import { renderCountryField } from "./RenderCountryField";
import { getFormValues, isInvalid } from "redux-form";

const validate = values => {
  const errors = {};
  if (!values.firstName) {
    errors.firstName = "Required";
  }
  if (!values.lastName) {
    errors.lastName = "Required";
  }
  if (!values.city) {
    errors.city = "Required";
  }
  if (!values.address) {
    errors.address = "Required";
  }
  if (!values.region) {
    errors.region = "Required";
  }
  if (!values.postalCode) {
    errors.postalCode = "Required";
  } else {
    if (
      (values.country == "United States" &&
        !/^[0-9]{5}(?:-[0-9]{4})?$/.test(values.postalCode)) ||
      (values.country == "Canada" &&
        !/^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/.test(values.postalCode))
    ) {
      errors.postalCode = "Invalid postal code";
    }
  }

if (!values.phone) {
    errors.phone = "Required";
  } else {
    if (
      (values.country == "United States" || values.country == "Canada") &&
      !/^[2-9]d{2}[2-9]d{2}d{4}$/.test(values.phone)
    ) {
      errors.phone = "Invalid phone";
    }
  }

if (!/[^@]+@[^.]+..+/.test(values.email)) {
    errors.email = "Invalid email";
  }
  if (Number.isNaN(+values.age) || values.age < 0 || values.age > 200) {
    errors.age = "Age must be between 0 and 200";
  }
  if (!values.country) {
    errors.country = "Required";
  }
  return errors;
};

function ContactForm({
  edit,
  onSave,
  setContacts,
  contact,
  onCancelAdd,
  onCancelEdit,
  invalid,
  values,
  currentContact,
  ...props
}) {
  const handleSubmit = async event => {
    if (invalid) {
      return;
    }
    if (!edit) {
      await addContact(values);
    } else {
      await editContact(values);
    }
    const response = await getContacts();
    setContacts(response.data);
    onSave();
  };

return (
    <div className="form">
      <Form noValidate onSubmit={props.handleSubmit(handleSubmit.bind(this))}>
        <Field
          name="firstName"
          type="text"
          component={renderInputField}
          label="First Name"
        />

        <Field
          name="lastName"
          type="text"
          component={renderInputField}
          label="Last Name"
        />

        <Field
          name="address"
          type="text"
          component={renderInputField}
          label="Address"
        />

        <Field
          name="city"
          type="text"
          component={renderInputField}
          label="City"
        />

        <Field
          name="region"
          type="text"
          component={renderInputField}
          label="Region"
        />

        <Field name="country" component={renderCountryField} label="Country" />

        <Field
          name="postalCode"
          type="text"
          component={renderInputField}
          label="Postal Code"
        />

        <Field
          name="phone"
          type="text"
          component={renderInputField}
          label="Phone"
        />

        <Field
          name="email"
          type="email"
          component={renderInputField}
          label="Email"
        />

        <Field
          name="age"
          type="text"
          component={renderInputField}
          label="Age"
        />

        <Button type="submit" style={{ marginRight: "10px" }}>
          Save
        </Button>
        <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
          Cancel
        </Button>
      </Form>
    </div>
  );
}

ContactForm.propTypes = {
  edit: PropTypes.bool,
  onSave: PropTypes.func,
  onCancelAdd: PropTypes.func,
  onCancelEdit: PropTypes.func,
  contact: PropTypes.object
};

const mapStateToProps = state => {
  return {
    contacts: state.contacts
  };
};

const mapDispatchToProps = dispatch => ({
  setContacts: contacts => dispatch(setContacts(contacts))
});

ContactForm = connect(state => ({
  values: getFormValues("syncValidation")(state),
  invalid: isInvalid("syncValidation")(state)
}))(ContactForm);

ContactForm = connect(
  mapStateToProps,
  mapDispatchToProps
)(ContactForm);

ContactForm = reduxForm({
  form: "syncValidation",
  validate,
  enableReinitialize: true
})(ContactForm);

ContactForm = connect(state => {
  return {
    initialValues: state.currentContact.data
  };
})(ContactForm);

export default ContactForm;

This is the form for editing and adding our contact. We use Redux Form extensively here. The Form component is provided by React Bootstrap. The Field component is from Redux Form. We pass in our custom input field components into the component prop of the field, along with the name , type , and label of our inputs. We will use them in our custom field components, renderInputField and renderCountryField .

In the onSubmit prop of the Form component, we wrap our own handleSubmit function with Redux Form’s handleSubmit function provided in the props so that we can use our own form submit handler instead of Redux Form’s handleSubmit function for handling form submissions.

In our handleSubmit function, we check for the form’s validity by getting the invalid prop from the props. The validation rules are in the validate function at the top of the code, which we passing into the reduxForm function at the bottom of our code. In the validate function, we get the form input values in the values parameter and set the errors object with the error messages of each field. We can check validation that depends on other fields easily in this function.

Once the form is checked to be valid, whereinvalid is false , then we call addContact or editContact by passing the value prop we get from the connect functions from Redux Form depending on if we are adding or not and save the data. Then we call getContacts and setContacts from mapDispatchToProps to update our Redux store with the latest contact entries from back end. Then call onSave , which is passed in from HomePage.js that we will create, to close the modal.

The invalid , and values props are generated by running:

ContactForm = connect(state => ({
  values: getFormValues("syncValidation")(state),
  invalid: isInvalid("syncValidation")(state)
}))(ContactForm);

This block:

ContactForm = reduxForm({
  form: "syncValidation",
  validate,
  enableReinitialize: true
})(ContactForm);

provides us with form validation capabilities of Redux Form in this component, and the initial values of the edit form is retrieved from our store by running:

ContactForm = connect(state => {
  return {
    initialValues: state.currentContact.data
  };
})(ContactForm);

Next, create exports.js and add:

export const COUNTRIES = [
  "Afghanistan",
  "Albania",
  "Algeria",
  "Andorra",
  "Angola",
  "Anguilla",
  "Antigua &amp; Barbuda",
  "Argentina",
  "Armenia",
  "Aruba",
  "Australia",
  "Austria",
  "Azerbaijan",
  "Bahamas",
  "Bahrain",
  "Bangladesh",
  "Barbados",
  "Belarus",
  "Belgium",
  "Belize",
  "Benin",
  "Bermuda",
  "Bhutan",
  "Bolivia",
  "Bosnia &amp; Herzegovina",
  "Botswana",
  "Brazil",
  "British Virgin Islands",
  "Brunei",
  "Bulgaria",
  "Burkina Faso",
  "Burundi",
  "Cambodia",
  "Cameroon",
  "Canada",
  "Cape Verde",
  "Cayman Islands",
  "Chad",
  "Chile",
  "China",
  "Colombia",
  "Congo",
  "Cook Islands",
  "Costa Rica",
  "Cote D Ivoire",
  "Croatia",
  "Cruise Ship",
  "Cuba",
  "Cyprus",
  "Czech Republic",
  "Denmark",
  "Djibouti",
  "Dominica",
  "Dominican Republic",
  "Ecuador",
  "Egypt",
  "El Salvador",
  "Equatorial Guinea",
  "Estonia",
  "Ethiopia",
  "Falkland Islands",
  "Faroe Islands",
  "Fiji",
  "Finland",
  "France",
  "French Polynesia",
  "French West Indies",
  "Gabon",
  "Gambia",
  "Georgia",
  "Germany",
  "Ghana",
  "Gibraltar",
  "Greece",
  "Greenland",
  "Grenada",
  "Guam",
  "Guatemala",
  "Guernsey",
  "Guinea",
  "Guinea Bissau",
  "Guyana",
  "Haiti",
  "Honduras",
  "Hong Kong",
  "Hungary",
  "Iceland",
  "India",
  "Indonesia",
  "Iran",
  "Iraq",
  "Ireland",
  "Isle of Man",
  "Israel",
  "Italy",
  "Jamaica",
  "Japan",
  "Jersey",
  "Jordan",
  "Kazakhstan",
  "Kenya",
  "Kuwait",
  "Kyrgyz Republic",
  "Laos",
  "Latvia",
  "Lebanon",
  "Lesotho",
  "Liberia",
  "Libya",
  "Liechtenstein",
  "Lithuania",
  "Luxembourg",
  "Macau",
  "Macedonia",
  "Madagascar",
  "Malawi",
  "Malaysia",
  "Maldives",
  "Mali",
  "Malta",
  "Mauritania",
  "Mauritius",
  "Mexico",
  "Moldova",
  "Monaco",
  "Mongolia",
  "Montenegro",
  "Montserrat",
  "Morocco",
  "Mozambique",
  "Namibia",
  "Nepal",
  "Netherlands",
  "Netherlands Antilles",
  "New Caledonia",
  "New Zealand",
  "Nicaragua",
  "Niger",
  "Nigeria",
  "Norway",
  "Oman",
  "Pakistan",
  "Palestine",
  "Panama",
  "Papua New Guinea",
  "Paraguay",
  "Peru",
  "Philippines",
  "Poland",
  "Portugal",
  "Puerto Rico",
  "Qatar",
  "Reunion",
  "Romania",
  "Russia",
  "Rwanda",
  "Saint Pierre &amp; Miquelon",
  "Samoa",
  "San Marino",
  "Satellite",
  "Saudi Arabia",
  "Senegal",
  "Serbia",
  "Seychelles",
  "Sierra Leone",
  "Singapore",
  "Slovakia",
  "Slovenia",
  "South Africa",
  "South Korea",
  "Spain",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Sudan",
  "Suriname",
  "Swaziland",
  "Sweden",
  "Switzerland",
  "Syria",
  "Taiwan",
  "Tajikistan",
  "Tanzania",
  "Thailand",
  "Timor L'Este",
  "Togo",
  "Tonga",
  "Trinidad &amp; Tobago",
  "Tunisia",
  "Turkey",
  "Turkmenistan",
  "Turks &amp; Caicos",
  "Uganda",
  "Ukraine",
  "United Arab Emirates",
  "United Kingdom",
  "United States",
  "United States Minor Outlying Islands",
  "Uruguay",
  "Uzbekistan",
  "Venezuela",
  "Vietnam",
  "Virgin Islands (US)",
  "Yemen",
  "Zambia",
  "Zimbabwe"
];

so we get an array of countries for our country field drop down.

Next in HomePage.css , we add:

.home-page {
    padding: 20px;
}

to get our page some padding.

Then we create HomePage.js and add:

import React from "react";
import { useState, useEffect } from "react";
import Table from "react-bootstrap/Table";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { connect } from "react-redux";
import { getContacts, deleteContact } from "./requests";
import { setCurrentContact } from "./actionCreators";

function HomePage({ setCurrentContact }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedContact, setSelectedContact] = useState({});
  const [contacts, setContacts] = useState([]);

  const openModal = () => {
    setOpenAddModal(true);
  };

  const closeModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
    getData();
  };

  const cancelAddModal = () => {
    setOpenAddModal(false);
  };

  const editContact = contact => {
    setSelectedContact(contact);
    setCurrentContact(contact);
    setOpenEditModal(true);
  };

  const cancelEditModal = () => {
    setOpenEditModal(false);
  };

  const getData = async () => {
    const response = await getContacts();
    setContacts(response.data);
    setInitialized(true);
  };

  const deleteSelectedContact = async id => {
    await deleteContact(id);
    getData();
  };

  useEffect(() => {
    if (!initialized) {
      getData();
    }
  });

  return (
    <div className="home-page">
      <h1>Contacts</h1>
      <Modal show={openAddModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={false}
            onSave={closeModal.bind(this)}
            onCancelAdd={cancelAddModal}
          />
        </Modal.Body>
      </Modal>

      <Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={true}
            onSave={closeModal.bind(this)}
            contact={selectedContact}
            onCancelEdit={cancelEditModal}
          />
        </Modal.Body>
      </Modal>
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Contact</Button>
      </ButtonToolbar>
      <br />
      <div className="table-responsive">
        <Table striped bordered hover>
          <thead>
            <tr>
              <th>First Name</th>
              <th>Last Name</th>
              <th>Address</th>
              <th>City</th>
              <th>Country</th>
              <th>Postal Code</th>
              <th>Phone</th>
              <th>Email</th>
              <th>Age</th>
              <th>Edit</th>
              <th>Delete</th>
            </tr>
          </thead>
          <tbody>
            {contacts.map(c => (
              <tr key={c.id}>
                <td>{c.firstName}</td>
                <td>{c.lastName}</td>
                <td>{c.address}</td>
                <td>{c.city}</td>
                <td>{c.country}</td>
                <td>{c.postalCode}</td>
                <td>{c.phone}</td>
                <td>{c.email}</td>
                <td>{c.age}</td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editContact.bind(this, c)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSelectedContact.bind(this, c.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </Table>
      </div>
    </div>
  );
}

const mapStateToProps = state => {
  return {
    contacts: state.contacts
  };
};

const mapDispatchToProps = dispatch => ({
  setCurrentContact: contact => dispatch(setCurrentContact(contact))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomePage);

This is the home page of our app. We have a button to open the modal and a table to display the address book entries. Also, we have functions to open and close the modal with the openModal , closeModal , cancelAddModal , and cancelEditModal functions. We have a modal for add and another for editing contacts, they open the same form. When the edit modal is open with the editContact function, setCurrentContact provided by the mapDispatchToProps function via the props is run, setting the current contact being edited so that it can be retrieved for the initialValues of our ContactForm component by calling the connect function like we did. We have Edit and Delete buttons to call editContact and deleteSelectedContact respectively.

In index.js , replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { createStore, combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";
import { Provider } from "react-redux";
import { contactsReducer, currentContactReducer } from "./reducers";

const rootReducer = combineReducers({
  form: formReducer,
  contacts: contactsReducer,
  currentContact: currentContactReducer
});

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)
serviceWorker.unregister();

to inject our Redux store so that it’s available throughout our app.

Then create reducers.js and add:

import { SET_CONTACTS, LOAD } from "./actions";

function contactsReducer(state = {}, action) {
  switch (action.type) {
    case SET_CONTACTS:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state;
  }
}

function currentContactReducer(state = {}, action) {
  switch (action.type) {
    case LOAD:
      return {
        data: action.data
      };
    default:
      return state;
  }
}

export { contactsReducer, currentContactReducer };

to create the 2 reducers that we mentioned before.

Next, create RenderCountryField.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import { COUNTRIES } from "./exports";

export const renderCountryField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <Form.Row>
    <Form.Group as={Col} md="12" controlId="country">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        as="select"
        type={type}
        placeholder={label}
        {...input}
        isInvalid={touched && error}
      >
        {COUNTRIES.map(c => (
          <option key={c} value={c}>
            {c}
          </option>
        ))}
      </Form.Control>
      <Form.Control.Feedback type="invalid">
        {touched && error}
      </Form.Control.Feedback>
    </Form.Group>
  </Form.Row>
);

to create our country drop-down with form validation, which we pass into the component prop of the Field component in ContactForm.js .

Next create RenderInputField.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";

export const renderInputField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <Form.Row>
    <Form.Group as={Col} md="12">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        type={type}
        placeholder={label}
        {...input}
        isInvalid={touched && error}
      />
      <Form.Control.Feedback type="invalid">
        {touched &&
          ((error && <span>{error}</span>) ||
            (warning && <span>{warning}</span>))}
      </Form.Control.Feedback>
    </Form.Group>
  </Form.Row>
);

to create the text input for our contact form.

Both inputs use the Form component from React Bootstrap.

Next, create requests.js and add:

const APIURL = 'http://localhost:3000';
const axios = require('axios');
export const getContacts = () => axios.get(`${APIURL}/contacts`);

export const addContact = (data) => axios.post(`${APIURL}/contacts`, data);

export const editContact = (data) => axios.put(`${APIURL}/contacts/${data.id}`, data);

export const deleteContact = (id) => axios.delete(`${APIURL}/contacts/${id}`);

to create the functions for making the HTTP requests to our back end to persist our contact data.

Finally, in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React Address Book App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to change our app title and add our Bootstrap CSS.

Now we can run the app by running set PORT=3001 && react-scripts start on Windows or PORT=3006 react-scripts start on Linux.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "contacts": [
  ]
}

So we have the contacts endpoints defined in the requests.js available.

Categories
JavaScript JavaScript Basics

Cloning Arrays in JavaScript

There are a few ways to clone an array in JavaScript,

Object.assign

Object.assign allows us to make a shallow copy of any kind of object including arrays.

For example:

const a = [1,2,3];
const b = Object.assign([], a); // [1,2,3]

Array.slice

The Array.slice function returns a copy of the original array.

For example:

const a = [1,2,3];
const b = a.slice(0); // [1,2,3]

Array.from

The Array.slice function returns a copy of the original array. It takes array like objects like Set and it also takes an array as an argument.

const a = [1,2,3];
const b = Array.from(a); // [1,2,3]

Spread Operator

The fastest way to copy an array, which is available with ES6 or later, is the spread operator.

const a = [1,2,3];
const b = [...a]; // [1,2,3]

JSON.parse and JSON.stringify

This allows for deep copy of an array and only works if the objects in the array are plain objects. It can be used like this:

const a = [1,2,3];
const b = JSON.parse(JSON.stringify(a)); // [1,2,3]