MEAN Stack Authentication: How to create the frontend using Angular 9

MEAN Stack Authentication: How to create the frontend using Angular 9

In this MEAN Stack Authentication tutorial explains how to implement user authentication using REST APIs and JWT tokens in a MEAN stack web application. Learn how to create the frontend using Angular 9. Learn how to deal with user authentication in the MEAN stack. Our MEAN architecture comprises an Angular 9 app connected to a REST API built with Node, Express and MongoDB.

In this step by step tutorial, we'll be building an example app with JWT authentication and REST APIs based on the MEAN stack. We'll be using Angular 9 for the frontend and Node.js along with Express and MongoDB in the backend.

In this tutorial, we'll particularly learn how to build the frontend and we'll be using the backend from this example

What is the MEAN stack?

We'll be look at how to deal with user authentication in the MEAN stack. Our MEAN architecture comprises an Angular 9 app connected to a REST API built with Node, Express and MongoDB.

According to [Wikipedia](https://en.wikipedia.org/wiki/MEAN_(software_bundle):

MEAN is a free and open-source JavaScript software stack for building dynamic web sites and web applications. The MEAN stack is MongoDB, Express.js, AngularJS (or Angular), and Node.js. Because all components of the MEAN stack support programs that are written in JavaScript, MEAN applications can be written in one language for both server-side and client-side execution environments.

The MEAN stack authentication flow

This is how authentication works in a MEAN stack app:

  • The flow starts from the Angular 9 application where users send the REST API implemetning the JWT authentication endpoints,
  • the Node/Express auth endpoint generates JWT tokens upon registration or login, and send them back to the Angular 9 application
  • the Angular application uses local storage to persist the JWT token,
  • the Angular 9 application verifies the JWT tokens when rendering protected views
  • the Angular application sends the JWT token back to Node auth server when accessing protected API routes/resources.
The steps of our Angular 9 tutorial

These are the steps of this tutorial:

  1. Step 1- Installing Angular CLI and creating an Angular 9 project
  2. Step 2 - Creating Angular 9 components
  3. Step 3 - Installing Bootstrap for styling
  4. Step 4 - Setting up the Node authentication backend
  5. Step 5 - Setting up Angular 9 HttpClient
  6. Step 6 - Creating the user authentication service
  7. Step 7 - Attaching the JWT access token to requests using Angular 9 Http Interceptors
  8. Step 8 - Guarding/protecting routes from non authorized access
  9. Step 9 - Setting up reactive forms
  10. Step 10 - Adding the registration and login forms
  11. Step 11 - Getting the user profile
  12. Step 12 - Adding the logout button

Let's get started!

Step 1 - Installing Angular CLI and creating an Angular 9 project

In this step, we'll install Angular 9 CLI and initialize a project.

$ npm install --global @angular/[email protected]
$ ng new angular-node-authentication-example

# ? Would you like to add Angular routing? Yes
# ? Which stylesheet format would you like to use? CSS

The @next tag is required to install Angular 9 CLI at the pre-release version.

At the time of writing this tutorial @angular/cli v9.0.0-rc is installed in our machine.

Next, navigate inside your project's folder and serve the application locally using the following commands :

$ cd angular-node-authentication-example
$ ng serve

The development server will be serving our Angular 9 app from the http://localhost:4200/ address.

Step 2 - Creating Angular 9 components

In this step, we'll create the components of our application.

Our Angular app will have the login, register and user-profile pages.

Open a new command-line interface and run the following commands to create the components composing the UI of our app:

$ ng generate component login
$ ng generate component register
$ ng generate component user-profile

Open the src/app/app-routing.module.ts file and import the components then add them to routes array as follows:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { UserProfileComponent } from './user-profile/user-profile.component';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'profile/:id', component: UserProfileComponent }
];

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

export class AppRoutingModule { }

Step 3 - Installing Bootstrap for styling

In this step, we'll install Bootstrap in our Angular project.

In your terminal run the following command:

$ npm install --save bootstrap

Next, add the Bootstrap 4 stylesheet path to the angular.json file as follows:

"styles": [
          "node_modules/bootstrap/dist/css/bootstrap.min.css",
          "src/styles.css"
         ]

Step 4 - Setting up the Node authentication backend

In this step, we'll clone the Node authentication backend from GitHub and run it locally.

Head back to a new command-line interface and run the following command:

$ git clone https://github.com/techiediaries/node-mongodb-jwt-authentication.git node-mongodb-authentication-backend

Next, navigate to the project's folder and install the dependencies then start the server as follows:

$ cd node-mongodb-authentication-backend
$ npm install
$ npm start

The Node server will be available from http://localhost:4000/.

Next, you also need to run the mongod client. Open a new command-line interface and run:

$ mongod

These are the API endpoints that are exposed from the Node server:

  • POST /users/login
  • POST /users/register
  • GET /users/profile/id
  • PUT /users/update/id
  • DELETE /users/delete/id
Step 5 - Setting up Angular 9 HttpClient

In this step we'll import and set up HttpClient in our Angular project.

Open the src/app/app.module.ts file, import HttpClientModule and add it the imports array as follows:

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule
   ]
})

Step 6 - Creating the user authentication service

In this step, we'll create the user authentication service.

First, we need to create the User model inside a src/app/user.ts file as follows:

export class User {
    _id: String;
    name: String;
    email: String;
    password: String;
}

Next, head back to your command-line interface and run the following command:

$ ng generate service auth

Next, open the generated src/app/auth.service.ts file and update it as follows:

import { Injectable } from '@angular/core';

import { Router } from '@angular/router';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';

import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

import { User } from './user';

@Injectable({
  providedIn: 'root'
})

export class AuthService {
  API_URL: string = 'http://localhost:4000';
  headers = new HttpHeaders().set('Content-Type', 'application/json');
  currentUser = {};

  constructor(private httpClient: HttpClient,public router: Router){}

  register(user: User): Observable<any> {

    return this.httpClient.post(`${this.API_URL}/users/register`, user).pipe(
        catchError(this.handleError)
    )
  }

  login(user: User) {
    return this.httpClient.post<any>(`${this.API_URL}/users/login`, user)
      .subscribe((res: any) => {
        localStorage.setItem('access_token', res.token)
        this.getUserProfile(res._id).subscribe((res) => {
          this.currentUser = res;
          this.router.navigate(['users/profile/' + res.msg._id]);
        })
      })
  }

  getAccessToken() {
    return localStorage.getItem('access_token');
  }

  get isLoggedIn(): boolean {
    let authToken = localStorage.getItem('access_token');
    return (authToken !== null) ? true : false;
  }

  logout() {
    if (localStorage.removeItem('access_token') == null) {
      this.router.navigate(['users/login']);
    }
  }

  getUserProfile(id): Observable<any> {
    return this.httpClient.get(`${this.API_URL}/users/profile/${id}`, { headers: this.headers }).pipe(
      map((res: Response) => {
        return res || {}
      }),
      catchError(this.handleError)
    )
  }

  handleError(error: HttpErrorResponse) {
    let msg = '';
    if (error.error instanceof ErrorEvent) {
      // client-side error
      msg = error.error.message;
    } else {
      // server-side error
      msg = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    return throwError(msg);
  }
}

We first import the necessary APIs like Router, HttpClient, HttpHeaders, HttpErrorResponse, Observable, throwError, catchError, map and the User class.

Next, we inject HttpClient via the service constructor and we define the API_URL, headers and currentUser variables. Next, we define the following methods:

  • The register() method which sends a POST request to the users/register endpoint for creating a user in MongoDB with information like name, email and password.
  • The login() method which sends a POST request to the users/loginendpoint and receives an HTTP responce with a JWT access token that will be used to allow the user to access the protected resources on the server.
  • The getAccessToken() method for accessing the token stored in the local storage after user login.
  • The isLoggedIn() method which returns true if the user is logged in or otherwise false.
  • The logout() method used to remove the access token from local storage and redirects the user to the login page.
  • The getUserProfile() method used to send a GET request to retrive the user profile,
  • The handleError() method used to handle any errors.
Step 7 - Attaching the JWT access token to requests using Angular 9 Http Interceptors

In this step, we'll create ann HTTP interceptor that will be used to attach the JWT access token to the authorization header of the ongoing requests.

Create the src/app/auth.interceptor.ts file and add the following code:

import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpRequest, HttpHandler } from "@angular/common/http";
import { AuthService } from "./auth.service";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private authService: AuthService) { }

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        const accessToken = this.authService.getAccessToken();
        req = req.clone({
            setHeaders: {
                Authorization: `JWT $[accessToken}` 
            }
        });
        return next.handle(req);
    }
}

We first import the necessary APIs such as Injectable , HttpInterceptor, HttpRequest, HttpHandler and AuthService. Next, we define the interceptor class and we decorate it with @Injectable, we inject the auth service via the constructor and we add the intercept() method where we call the getAccessToken() method to retrive the JWT token from local stoage and add it to the Authorization header of the outgoing request.

Next, we need to provide this interceptor in our app module. Open the src/app/app.module.ts file, import the interceptor class and add it to the providers array as follows:

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';

@NgModule({
  declarations: [...],
  imports: [HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ],
  bootstrap: [...]
})

export class AppModule { }

Step 8 - Guarding/protecting routes from non authorized access

In this step, we'll create and set an authentication guard that will be used to protect the users/profile/ route from non loggedin users.

Head back to your command-line interface and run the following command:

$ ng generate guard auth

Next, open the src/app/auth.guard.ts file and add the following code:

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

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    public authService: AuthService,
    public router: Router
  ) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    if (this.authService.isLoggedIn() !== true) {
      window.alert("Access not allowed!");
      this.router.navigate(['users/login'])
    }
    return true;
  }
}

Open the src/app/app-routing.module.ts file and import the authentication guard and apply it to the route as follows:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { UserProfileComponent } from './user-profile/user-profile.component';

import { AuthGuard } from "./auth.guard";

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'profile/:id', component: UserProfileComponent, canActivate: [AuthGuard] }
];

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

export class AppRoutingModule { }

Step 9 - Setting up reactive forms

In this step, we'll import and set up the reactive forms module in Angular project.

Open the src/app/app.module.ts file and import both ReactiveFormsModule and FormsModule then add them to the imports array:

import { ReactiveFormsModule, FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ReactiveFormsModule,
    FormsModule
  ],
})

export class AppModule { }

Step 10 - Adding the registration and login forms

In this step, we'll create the registration and login forms to our components.

Open the src/app/register.component.ts file and add the following code:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from "@angular/forms";
import { AuthService } from './auth.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})

export class RegisterComponent implements OnInit {
  registerForm: FormGroup;

  constructor(
    public formBuilder: FormBuilder,
    public authService: AuthService,
    public router: Router
  ) {
    this.registerForm= this.formBuilder.group({
      name: [''],
      email: [''],
      password: ['']
    })
  }

  ngOnInit() { }

  registerUser() {
    this.authService.register(this.registerForm.value).subscribe((res) => {
      if (res.result) {
        this.registerForm.reset()
        this.router.navigate(['login']);
      }
    })
  }
}

Next, open the src/app/register.component.html file and add the following code:

<div class="auth-wrapper">
    <form class="form-register" [formGroup]="registerForm" (ngSubmit)="registerUser()">
        <h3 class="h3 mb-3 font-weight-normal text-center">Register</h3>
        <div class="form-group">
            <label>Name</label>
            <input type="text" class="form-control" formControlName="name" placeholder="Enter name" required>
        </div>
        <div class="form-group">
            <label>Email address</label>
            <input type="email" class="form-control" formControlName="email" placeholder="Enter email" required>
        </div>
        <div class="form-group">
            <label>Password</label>
            <input type="password" class="form-control" formControlName="password" placeholder="Password" required>
        </div>
        <button type="submit" class="btn btn-block btn-primary">Register!</button>
    </form>
</div>

Next, let's add the login form. Open the src/app/login.component.ts file and update it as follows:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from "@angular/forms";

import { AuthService } from './auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})

export class LoginComponent implements OnInit {
  loginForm: FormGroup;

  constructor(
    public formBuilder: FormBuilder,
    public authService: AuthService,
    public router: Router
  ) {
    this.loginForm= this.formBuilder.group({
      email: [''],
      password: ['']
    })
  }

  ngOnInit() { }

  loginUser() {
    this.authService.login(this.loginForm.value)
  }
}

Next, open the src/app/login.component.html file and add the following code:

<div>
    <form class="form-login" [formGroup]="loginForm" (ngSubmit)="loginUser()">
        <h3 class="h3 mb-3 font-weight-normal text-center">Login</h3>
        <div class="form-group">
            <label>Email</label>
            <input type="email" class="form-control" formControlName="email" placeholder="Enter email" required>
        </div>
        <div class="form-group">
            <label>Password</label>
            <input type="password" class="form-control" formControlName="password" placeholder="Password">
        </div>
        <button type="submit" class="btn btn-block btn-primary">Login!</button>
    </form>
</div>

Step 11 - Getting the user profile

In this step, we'll get and display the user profile when the user is successfully logged in.

For example try to access the /user-profile/_id Angular URL without providing the invalid token. You will find out that server doesn’t render the user data.

Open the src/app/user-profile/user-profile.component.ts file and add the following code:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})

export class UserProfileComponent implements OnInit {
  currentUser: Object = {};

  constructor(
    public authService: AuthService,
    private activatedRoute: ActivatedRoute
  ) {
    let id = this.activatedRoute.snapshot.paramMap.get('id');
    this.authService.getUserProfile(id).subscribe(res => {
      this.currentUser = res.msg;
    })
  }

  ngOnInit() { }
}

Next, open the src/app/user-profile/user-profile.component.html file and add the following code:

<div class="container">
    <div class="row">
        <div class="col-xs-12">
            <h2 class="mb-4">Your User Information</h2>
            <p><strong>Name:</strong> </p>
            <p><strong>Email:</strong> </p>
        </div>
    </div>
</div>

Step 12 - Adding the logout button

In this step, we will add the logout, hiding and showing nav items in our MEAN stack user authentication app.

Open the src/app/app.component.ts file, import and inject AuthService and add the logout() method as follows:

import { Component } from '@angular/core';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {

  constructor(public authService: AuthService) { }

  logout() {
    this.authService.logout()
  }
}

Next, open the src/app/app.component.html file and add update it as follows:

<div>
  <button (click)="logout()" *ngIf="this.authService.isLoggedIn()" type="button" class="btn btn-primary">Logout</button>
</div>

<router-outlet></router-outlet>

Conclusion

In this tutorial, we've seen by example how to implement user authentication using REST APIs and JWT tokens in a MEAN stack web application. We've particularly looked how to create the frontend using Angular 9.

Node, Express, Angular 7, GraphQL and MongoDB CRUD Web App

Node, Express, Angular 7, GraphQL and MongoDB CRUD Web App

In this tutorial, we will go to the walkthrough of building GraphQL query language API for communication between Node-Express-MongoDB on Server side and Angular 7 on the Client side.

In this tutorial, we will go to the walkthrough of building GraphQL query language API for communication between Node-Express-MongoDB on Server side and Angular 7 on the Client side.

The comprehensive step by step tutorial on building CRUD (Create, Read, Update, Delete) Web Application using Node.js, Express.js, Angular 7, MongoDB and GraphQL. This is our first tutorial that using GraphQL, you can find more reference and guide on their official site.

On the server side, we are using Express-Graphql modules and it’s dependencies. For the client side, we are using Apollo Angular modules and dependencies.

Table of Contents:
  • Create Express.js App
  • Install and Configure Mongoose.js Modules for Accessing MongoDB
  • Create Mongoose.js Model for the Book Document
  • Install GraphQL Modules and Dependencies
  • Create GraphQL Schemas for the Book
  • Add Mutation for CRUD Operation to the Schema
  • Test GraphQL using GraphiQL
  • Create Angular 7 Application
  • Install and Configure Required Modules and Dependencies
  • Create Routes for Navigation between Angular Pages/Component
  • Display List of Books using Angular 7 Material
  • Show and Delete Books
  • Add a New Book using Angular 7 Material
  • Edit a Book using Angular 7 Material
  • Run and Test GraphQL CRUD from the Angular 7 Application

The following tools, frameworks, and modules are required for this tutorial:

We assume that you have installed Node.js. Now, we need to check the Node.js and NPM versions. Open the terminal or Node command line then type this commands.

node -v
v8.12.0
npm -v
6.4.1

That’s the Node.js and NPM version that we are using. Now, you can go to the main steps.

1. Create Express.js App

If Express.js Generator hasn’t installed, type this command from the terminal or Node.js command prompt.

sudo npm install express-generator -g

The sudo keyword is using in OSX or Linux Terminal otherwise you can use that command without sudo. Before we create an Express.js app, we have to create a root project folder inside your projects folder. From the terminal or Node.js command prompt, type this command at your projects folder.

mkdir node-graphql

Go to the newly created directory.

cd ./node-graphql

From there, type this command to generate Express.js application.

express server

Go to the newly created Express.js app folder.

cd ./server

Type this command to install all required NPM modules that describe in package.json dependencies.

npm install

To check the Express.js app running smoothly, type this command.

nodemon

or

npm start

If you see this information in the terminal or command prompt that means your Express.js app is ready to use.

[nodemon] 1.18.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./bin/www`

2. Install and Configure Mongoose.js Modules for Accessing MongoDB

To install Mongoose.js and it’s required dependencies, type this command.

npm install mongoose bluebird --save

Next, open and edit app.js then declare the Mongoose module.

var mongoose = require('mongoose');

Create a connection to the MongoDB server using this lines of codes.

mongoose.connect('mongodb://localhost/node-graphql', { promiseLibrary: require('bluebird'), useNewUrlParser: true })
  .then(() =>  console.log('connection successful'))
  .catch((err) => console.error(err));

Now, if you re-run again Express.js server after running MongoDB server or daemon, you will see this information in the console.

[nodemon] 1.18.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./bin/www`
connection successful

That’s mean, the connection to the MongoDB is successful.

3. Create Mongoose.js Model for the Book Document

Before creating a Mongoose.js model that represent Book Document, we have to create a folder at the server folder for hold Models. After that, we can create a Mongoose.js model file.

mkdir models
touch models/Book.js

Open and edit server/models/Book.js then add these lines of codes.

var mongoose = require('mongoose');

var BookSchema = new mongoose.Schema({
  id: String,
  isbn: String,
  title: String,
  author: String,
  description: String,
  published_year: { type: Number, min: 1945, max: 2019 },
  publisher: String,
  updated_date: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Book', BookSchema);

4. Install GraphQL Modules and Dependencies

Now, the GraphQL time. Type this command to install GraphQL modules and it’s dependencies.

npm install express express-graphql graphql cors --save

Next, open and edit server/app.js then declare all of those modules and dependencies.

var graphqlHTTP = require('express-graphql');
var schema = require('./graphql/bookSchema');
var cors = require("cors");

The schema is not created yet, we will create it in the next steps. Next, add these lines of codes for configuring GraphQL that can use over HTTP.

app.use('*', cors());
app.use('/graphql', cors(), graphqlHTTP({
  schema: schema,
  rootValue: global,
  graphiql: true,
}));

That’s configuration are enabled CORS and the GraphiQL. GraphiQL is the user interface for testing GraphQL query.

5. Create GraphQL Schemas for the Book

Create a folder at the server folder for hold GraphQL Schema files then create a Javascript file for the schema.

mkdir graphql
touch graphql/bookSchemas.js

Next, open and edit server/graphql/bookSchemas.js then declares all required modules and models.

var GraphQLSchema = require('graphql').GraphQLSchema;
var GraphQLObjectType = require('graphql').GraphQLObjectType;
var GraphQLList = require('graphql').GraphQLList;
var GraphQLObjectType = require('graphql').GraphQLObjectType;
var GraphQLNonNull = require('graphql').GraphQLNonNull;
var GraphQLID = require('graphql').GraphQLID;
var GraphQLString = require('graphql').GraphQLString;
var GraphQLInt = require('graphql').GraphQLInt;
var GraphQLDate = require('graphql-date');
var BookModel = require('../models/Book');

Create a GraphQL Object Type for Book models.

var bookType = new GraphQLObjectType({
  name: 'book',
  fields: function () {
    return {
      _id: {
        type: GraphQLString
      },
      isbn: {
        type: GraphQLString
      },
      title: {
        type: GraphQLString
      },
      author: {
        type: GraphQLString
      },
      description: {
        type: GraphQLString
      },
      published_year: {
        type: GraphQLInt
      },
      publisher: {
        type: GraphQLString
      },
      updated_date: {
        type: GraphQLDate
      }
    }
  }
});

Next, create a GraphQL query type that calls a list of book and single book by ID.

var queryType = new GraphQLObjectType({
  name: 'Query',
  fields: function () {
    return {
      books: {
        type: new GraphQLList(bookType),
        resolve: function () {
          const books = BookModel.find().exec()
          if (!books) {
            throw new Error('Error')
          }
          return books
        }
      },
      book: {
        type: bookType,
        args: {
          id: {
            name: '_id',
            type: GraphQLString
          }
        },
        resolve: function (root, params) {
          const bookDetails = BookModel.findById(params.id).exec()
          if (!bookDetails) {
            throw new Error('Error')
          }
          return bookDetails
        }
      }
    }
  }
});

Finally, exports this file as GraphQL schema by adding this line at the end of the file.

module.exports = new GraphQLSchema({query: queryType});

6. Add Mutation for CRUD Operation to the Schema

For completing CRUD (Create, Read, Update, Delete) operation of the GraphQL, we need to add a mutation that contains create, update and delete operations. Open and edit server/graphql/bookSchemas.js then add this mutation as GraphQL Object Type.

var mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: function () {
    return {
      addBook: {
        type: bookType,
        args: {
          isbn: {
            type: new GraphQLNonNull(GraphQLString)
          },
          title: {
            type: new GraphQLNonNull(GraphQLString)
          },
          author: {
            type: new GraphQLNonNull(GraphQLString)
          },
          description: {
            type: new GraphQLNonNull(GraphQLString)
          },
          published_year: {
            type: new GraphQLNonNull(GraphQLInt)
          },
          publisher: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: function (root, params) {
          const bookModel = new BookModel(params);
          const newBook = bookModel.save();
          if (!newBook) {
            throw new Error('Error');
          }
          return newBook
        }
      },
      updateBook: {
        type: bookType,
        args: {
          id: {
            name: 'id',
            type: new GraphQLNonNull(GraphQLString)
          },
          isbn: {
            type: new GraphQLNonNull(GraphQLString)
          },
          title: {
            type: new GraphQLNonNull(GraphQLString)
          },
          author: {
            type: new GraphQLNonNull(GraphQLString)
          },
          description: {
            type: new GraphQLNonNull(GraphQLString)
          },
          published_year: {
            type: new GraphQLNonNull(GraphQLInt)
          },
          publisher: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve(root, params) {
          return BookModel.findByIdAndUpdate(params.id, { isbn: params.isbn, title: params.title, author: params.author, description: params.description, published_year: params.published_year, publisher: params.publisher, updated_date: new Date() }, function (err) {
            if (err) return next(err);
          });
        }
      },
      removeBook: {
        type: bookType,
        args: {
          id: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve(root, params) {
          const remBook = BookModel.findByIdAndRemove(params.id).exec();
          if (!remBook) {
            throw new Error('Error')
          }
          return remBook;
        }
      }
    }
  }
});

Finally, add this mutation to the GraphQL Schema exports.

module.exports = new GraphQLSchema({query: queryType, mutation: mutation});

7. Test GraphQL using GraphiQL

To test the queries and mutations of CRUD operations, re-run again the Express.js app then open the browser. Go to this address <a href="http://localhost:3000/graphql" target="_blank">http://localhost:3000/graphql</a> to open the GraphiQL User Interface.

To get the list of books, replace all of the text on the left pane with this GraphQL query then click the Play button.

To get a single book by ID, use this GraphQL query.

{
  book(id: "5c738dd4cb720f79497de85c") {
    _id
    isbn
    title
    author
    description
    published_year
    publisher
    updated_date
  }
}

To add a book, use this GraphQL mutation.

mutation {
  addBook(
    isbn: "12345678",
    title: "Whatever this Book Title",
    author: "Mr. Bean",
    description: "The short explanation of this Book",
    publisher: "Djamware Press",
    published_year: 2019
  ) {
    updated_date
  }
}

You will the response at the right pane like this.

{
  "data": {
    "addBook": {
      "updated_date": "2019-02-26T13:55:39.160Z"
    }
  }
}

To update a book, use this GraphQL mutation.

mutation {
  updateBook(
    id: "5c75455b146dbc2504b94012",
    isbn: "12345678221",
    title: "The Learning Curve of GraphQL",
    author: "Didin J.",
    description: "The short explanation of this Book",
    publisher: "Djamware Press",
    published_year: 2019
  ) {
    _id,
    updated_date
  }
}

You will see the response in the right pane like this.

{
  "data": {
    "updateBook": {
      "_id": "5c75455b146dbc2504b94012",
      "updated_date": "2019-02-26T13:58:35.811Z"
    }
  }
}

To delete a book by ID, use this GraphQL mutation.

mutation {
  removeBook(id: "5c75455b146dbc2504b94012") {
    _id
  }
}

You will see the response in the right pane like this.

{
  "data": {
    "removeBook": {
      "_id": "5c75455b146dbc2504b94012"
    }
  }
}

8. Create Angular 7 Application

Before creating an Angular 7 application, we have to install Angular 7 CLI first. Type this command to install it.

sudo npm install -g @angular/cli

Next, create a new Angular 7 Web Application using this Angular CLI command at the root of this project folder.

ng new client

If you get the question like below, choose Yes and SCSS (or whatever you like to choose).

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS

Next, go to the newly created Angular 7 project folder.

cd client

Type this command to run the Angular 7 application using this command.

ng serve

Open your browser then go to this address localhost:4200 to check if Angular 7 created successfully.

9. Install and Configure Required Modules and Dependencies

Now, we have to install and configure all of the required modules and dependencies. Type this command to install the modules.

npm install --save apollo-angular apollo-angular-link-http apollo-link apollo-client apollo-cache-inmemory graphql-tag graphql

Next, open and edit client/src/app/app.module.ts then add these imports.

import { HttpClientModule } from '@angular/common/http';
import { ApolloModule, Apollo } from 'apollo-angular';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';

Add these modules to the @NgModule imports.

imports: [
  ...,
  HttpClientModule,
  ApolloModule,
  HttpLinkModule,
  ...
],

Create a constructor inside class AppModule then inject above modules and create a connection to the GraphQL in the Express.js server.

export class AppModule {
  constructor(
    apollo: Apollo,
    httpLink: HttpLink
  ) {
     apollo.create({
      link: httpLink.create({ uri: 'http://localhost:3000/graphql'}),
      cache: new InMemoryCache()
    });
  }
}

10. Create Routes for Navigation between Angular Pages/Component

The Angular 7 routes already added when we create new Angular 7 application in the previous step. Before configuring the routes, type this command to create a new Angular 7 components.

ng g component books
ng g component books/detail
ng g component books/add
ng g component books/edit

Open client/src/app/app.module.ts then you will see those components imported and declared in @NgModule declarations. Next, open and edit src/app/app-routing.module.ts then add these imports.

import { BooksComponent } from './books/books.component';
import { DetailComponent } from './books/detail/detail.component';
import { AddComponent } from './books/add/add.component';
import { EditComponent } from './books/edit/edit.component';

Add these arrays to the existing empty array of routes constant.

const routes: Routes = [
  {
    path: 'books',
    component: BooksComponent,
    data: { title: 'List of Books' }
  },
  {
    path: 'books/detail/:id',
    component: DetailComponent,
    data: { title: 'Book Details' }
  },
  {
    path: 'books/add',
    component: AddComponent,
    data: { title: 'Add Book' }
  },
  {
    path: 'books/edit/:id',
    component: EditComponent,
    data: { title: 'Edit Book' }
  },
  { path: '',
    redirectTo: '/books',
    pathMatch: 'full'
  }
];

Open and edit client/src/app/app.component.html and you will see the existing router outlet. Next, modify this HTML page to fit the CRUD page.

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>

<div class="container">
  <router-outlet></router-outlet>
</div>

Finally, open and edit src/app/app.component.scss then replace all SASS codes with this.

.container {
  padding: 20px;
}

11. Display List of Books using Angular 7 Material

We will be using Angular 7 Material as UI/UX component. First, we have to install these modules to the Angular 7 application. Type this Angular 7 Schema to install it.

ng add @angular/material

If there are questions like below, just use the default answer.

? Enter a prebuilt theme name, or "custom" for a custom theme: purple-green
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

We will register all required Angular Material components or modules to client/src/app/app.module.ts. Open and edit that file then add this imports.

import {
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule } from "@angular/material";

Of course we will use Angular 7 Reactive Form module, for that, modify FormsModule import to add ReactiveFormsModule.

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

Register the above modules to @NgModule imports array.

imports: [
  ...
  ReactiveFormsModule,
  BrowserAnimationsModule,
  MatInputModule,
  MatTableModule,
  MatPaginatorModule,
  MatSortModule,
  MatProgressSpinnerModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, to display a list of Books. Open and edit client/src/app/books/books.component.ts that previously generated then add these imports.

import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';
import { Book } from './book';

Declare all required variables for hold response, data, Angular Material table column and loading spinner control.

displayedColumns: string[] = ['title', 'author'];
data: Book[] = [];
resp: any = {};
isLoadingResults = true;

Inject the Apollo Angular module to the constructor.

constructor(private apollo: Apollo) {
}

Add a gql query inside ngOnInit() function.

ngOnInit() {
  this.apollo.query({
    query: gql `{ books { _id, title, author } }`
  }).subscribe(res => {
    this.resp = res;
    this.data = this.resp.data.books;
    console.log(this.data);
    this.isLoadingResults = false;
  });
}

Next, open and edit client/src/app/books/books.component.html then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" [routerLink]="['/books/add']"><mat-icon>add</mat-icon></a>
  </div>
  <div class="mat-elevation-z8">
    <table mat-table [dataSource]="data" class="example-table"
           matSort matSortActive="title" matSortDisableClear matSortDirection="asc">

      <!-- Product Name Column -->
      <ng-container matColumnDef="title">
        <th mat-header-cell *matHeaderCellDef>Title</th>
        <td mat-cell *matCellDef="let row">{{row.title}}</td>
      </ng-container>

      <!-- Product Price Column -->
      <ng-container matColumnDef="author">
        <th mat-header-cell *matHeaderCellDef>Author</th>
        <td mat-cell *matCellDef="let row">{{row.author}}</td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/books/detail/', row._id]"></tr>
    </table>
  </div>
</div>

Finally, add some styles for this page by open and edit client/src/app/books/books.component.scss then add these lines of SCSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-table-container {
  position: relative;
  max-height: 400px;
  overflow: auto;
}

table {
  width: 100%;
}

.example-loading-shade {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 56px;
  right: 0;
  background: rgba(0, 0, 0, 0.15);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.example-rate-limit-reached {
  color: #980000;
  max-width: 360px;
  text-align: center;
}

/* Column Widths */
.mat-column-number,
.mat-column-state {
  max-width: 64px;
}

.mat-column-created {
  max-width: 124px;
}

.mat-flat-button {
  margin: 5px;
}

12. Show and Delete Books

On the list of Books page we have a clickable row that can redirect to the show details page. Next, open and edit client/src/app/books/detail/detail.component.ts then add these imports.

import { ActivatedRoute, Router } from '@angular/router';
import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';
import { Book } from '../book';

Declare a constant variable before the class name for query and delete a book by ID.

const bookQuery = gql`
  query book($bookId: String) {
    book(id: $bookId) {
      _id
      isbn
      title
      author
      description
      published_year
      publisher
      updated_date
    }
  }
`;

const deleteBook = gql`
  mutation removeBook($id: String!) {
    removeBook(id:$id) {
      _id
    }
  }
`;

Next, declare all required variables before the constructor.

book: Book = { id: '', isbn: '', title: '', author: '', description: '', publisher: '', publishedYear: null, updatedDate: null };
isLoadingResults = true;
resp: any = {};
private query: QueryRef<any>;

Inject above imported modules to the constructor.

constructor(private apollo: Apollo, private router: Router, private route: ActivatedRoute) { }

Add a function for get a single Book data by ID.

getBookDetails() {
  const id = this.route.snapshot.params.id;
  this.query = this.apollo.watchQuery({
    query: bookQuery,
    variables: { bookId: id }
  });

  this.query.valueChanges.subscribe(res => {
    this.book = res.data.book;
    console.log(this.book);
    this.isLoadingResults = false;
  });
}

Call that function from ngOnInit function.

ngOnInit() {
  this.getBookDetails();
}

Add a function for delete a book by ID.

deleteBook() {
  this.isLoadingResults = true;
  const bookId = this.route.snapshot.params.id;
  this.apollo.mutate({
    mutation: deleteBook,
    variables: {
      id: bookId
    }
  }).subscribe(({ data }) => {
    console.log('got data', data);
    this.isLoadingResults = false;
    this.router.navigate(['/books']);
  }, (error) => {
    console.log('there was an error sending the query', error);
    this.isLoadingResults = false;
  });
}

For the view, open and edit client/src/app/books/detail/detail.component.html then replace all HTML tags with these lines of HTML tags.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" [routerLink]="['/books']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <mat-card-header>
      <mat-card-title><h2>{{book.title}}</h2></mat-card-title>
      <mat-card-subtitle>{{book.author}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <dl>
        <dt>ISBN:</dt>
        <dd>{{book.isbn}}</dd>
        <dt>Description:</dt>
        <dd>{{book.description}}</dd>
        <dt>Publisher:</dt>
        <dd>{{book.publisher}}</dd>
        <dt>Published Year:</dt>
        <dd>{{book.published_year}}</dd>
        <dt>Update Date:</dt>
        <dd>{{book.updated_date}}</dd>
      </dl>
    </mat-card-content>
    <mat-card-actions>
      <a mat-flat-button color="primary" [routerLink]="['/books/edit', book._id]"><mat-icon>edit</mat-icon></a>
      <a mat-flat-button color="warn" (click)="deleteBook(book._id)"><mat-icon>delete</mat-icon></a>
    </mat-card-actions>
  </mat-card>
</div>

To adjust the style, open and edit client/src/app/books/detail/detail.component.scss then add these lines of SCSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-loading-shade {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 56px;
  right: 0;
  background: rgba(0, 0, 0, 0.15);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.mat-flat-button {
  margin: 5px;
}

13. Add a New Book using Angular 7 Material

In the list of Book we have an Add button that will redirect to the Add Page. Next, open and edit client/src/app/books/add/add.component.ts then add these imports.

import { Router } from '@angular/router';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

Add a constant of gql query after the imports for submitting or post a new Book data.

const submitBook = gql`
  mutation addBook(
    $isbn: String!,
    $title: String!,
    $author: String!,
    $description: String!,
    $publisher: String!,
    $published_year: Int!) {
    addBook(
      isbn: $isbn,
      title: $title,
      author: $author,
      description: $description,
      publisher: $publisher,
      published_year: $published_year) {
      _id
    }
  }
`;

Declare all required variables before the constructor.

book: any = { isbn: '', title: '', author: '', description: '', publisher: '', publishedYear: null, updatedDate: null };
isLoadingResults = false;
resp: any = {};
bookForm: FormGroup;
isbn = '';
title = '';
author = '';
description = '';
publisher = '';
publishedYear: number = null;

Inject above imported modules to the constructor.

constructor(
  private apollo: Apollo,
  private router: Router,
  private formBuilder: FormBuilder
) { }

Initialize Angular 7 form group inside ngOnInit function.

ngOnInit() {
  this.bookForm = this.formBuilder.group({
    isbn : [null, Validators.required],
    title : [null, Validators.required],
    author : [null, Validators.required],
    description : [null, Validators.required],
    publisher : [null, Validators.required],
    publishedYear : [null, Validators.required]
  });
}

Add a function to get the form controls from the form group.

get f() {
  return this.bookForm.controls;
}

Add a function to submit or post a new book data.

onSubmit(form: NgForm) {
  this.isLoadingResults = true;
  const bookData = form.value;
  this.apollo.mutate({
    mutation: submitBook,
    variables: {
      isbn: bookData.isbn,
      title: bookData.title,
      author: bookData.author,
      description: bookData.description,
      publisher: bookData.publisher,
      published_year: bookData.publishedYear
    }
  }).subscribe(({ data }) => {
    console.log('got data', data);
    this.isLoadingResults = false;
    this.router.navigate(['/books/detail/', data.addBook._id]);
  }, (error) => {
    console.log('there was an error sending the query', error);
    this.isLoadingResults = false;
  });
}

Next, open and edit client/src/app/books/add/add.component.html then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" [routerLink]="['/books']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="bookForm" #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="ISBN" formControlName="isbn"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('isbn').valid && bookForm.get('isbn').touched">Please enter ISBN</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('title').valid && bookForm.get('title').touched">Please enter Title</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Author" formControlName="author"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('author').valid && bookForm.get('author').touched">Please enter Author</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <textarea matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher"></textarea>
        <mat-error>
          <span *ngIf="!bookForm.get('description').valid && bookForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Publisher" formControlName="publisher"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publisher').valid && bookForm.get('publisher').touched">Please enter Publisher</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Published Year" type="number" formControlName="publishedYear"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publishedYear').valid && bookForm.get('publishedYear').touched">Please enter Published Year</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!bookForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Give a litle style by open and edit client/src/app/books/add/add.component.scss then add this lines of SCSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child() {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}

14. Edit a Book using Angular 7 Material

We have put an edit button inside the Book Detail component for a redirect to Edit page. Now, open and edit client/src/app/books/edit/edit.component.ts then add these imports.

import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';

Add gql query before the class name for getting single Book by ID and submit book data.

const bookQuery = gql`
  query book($bookId: String) {
    book(id: $bookId) {
      _id
      isbn
      title
      author
      description
      published_year
      publisher
      updated_date
    }
  }
`;

const submitBook = gql`
  mutation updateBook(
    $id: String!,
    $isbn: String!,
    $title: String!,
    $author: String!,
    $description: String!,
    $publisher: String!,
    $published_year: Int!) {
    updateBook(
      id: $id,
      isbn: $isbn,
      title: $title,
      author: $author,
      description: $description,
      publisher: $publisher,
      published_year: $published_year) {
      updated_date
    }
  }
`;

Add all required variables before the constructor.

book: any = { _id: '', isbn: '', title: '', author: '', description: '', publisher: '', publishedYear: null, updatedDate: null };
isLoadingResults = true;
resp: any = {};
private query: QueryRef<any>;
bookForm: FormGroup;
id = '';
isbn = '';
title = '';
author = '';
description = '';
publisher = '';
publishedYear: number = null;

Inject above imported modules to the constructor.

constructor(
  private apollo: Apollo,
  private route: ActivatedRoute,
  private router: Router,
  private formBuilder: FormBuilder) { }

Initialize the Angular 7 form group to the ngOnInit function.

ngOnInit() {
  this.bookForm = this.formBuilder.group({
    isbn : [null, Validators.required],
    title : [null, Validators.required],
    author : [null, Validators.required],
    description : [null, Validators.required],
    publisher : [null, Validators.required],
    publishedYear : [null, Validators.required]
  });
}

Add a function to get the form controls from the form group.

get f() {
  return this.bookForm.controls;
}

Add a function to get a single book data by ID.

getBookDetails() {
  const id = this.route.snapshot.params.id;
  this.query = this.apollo.watchQuery({
    query: bookQuery,
    variables: { bookId: id }
  });

  this.query.valueChanges.subscribe(res => {
    this.book = res.data.book;
    console.log(this.book);
    this.id = this.book._id;
    this.isLoadingResults = false;
    this.bookForm.setValue({
      isbn: this.book.isbn,
      title: this.book.title,
      author: this.book.author,
      description: this.book.description,
      publisher: this.book.publisher,
      publishedYear: this.book.published_year
    });
  });
}

Call that function from the ngOnInit function.

ngOnInit() {
  ...
  this.getBookDetails();
}

Add a function for submitting the Book data to the GraphQL.

onSubmit(form: NgForm) {
  this.isLoadingResults = true;
  console.log(this.id);
  const bookData = form.value;
  this.apollo.mutate({
    mutation: submitBook,
    variables: {
      id: this.id,
      isbn: bookData.isbn,
      title: bookData.title,
      author: bookData.author,
      description: bookData.description,
      publisher: bookData.publisher,
      published_year: bookData.publishedYear
    }
  }).subscribe(({ data }) => {
    console.log('got data', data);
    this.isLoadingResults = false;
  }, (error) => {
    console.log('there was an error sending the query', error);
    this.isLoadingResults = false;
  });
}

Add a function to enter the Book details after click a Details button.

bookDetails() {
  this.router.navigate(['/books/detail/', this.id]);
}

Next, open and edit client/src/app/books/edit/edit.component.html then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" (click)="bookDetails()"><mat-icon>info</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="bookForm" #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="ISBN" formControlName="isbn"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('isbn').valid && bookForm.get('isbn').touched">Please enter ISBN</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('title').valid && bookForm.get('title').touched">Please enter Title</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Author" formControlName="author"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('author').valid && bookForm.get('author').touched">Please enter Author</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <textarea matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher"></textarea>
        <mat-error>
          <span *ngIf="!bookForm.get('description').valid && bookForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Publisher" formControlName="publisher"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publisher').valid && bookForm.get('publisher').touched">Please enter Publisher</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Published Year" type="number" formControlName="publishedYear"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publishedYear').valid && bookForm.get('publishedYear').touched">Please enter Published Year</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!bookForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

For styling, open and edit client/src/app/books/edit/edit.component.scss then add these lines of SCSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child() {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}

15. Run and Test GraphQL CRUD from the Angular 7 Application

Before the test, the GraphQL CRUD from the Angular 7 Application, just makes sure that you have run MongoDB server and Express.js server. If not yet, run those servers in different Terminal tabs. Next, run the Angular 7 application from the different terminal tabs.

ng serve

In the browser go to this URL localhost:4200 and here the whole application looks like.

That it’s, we have finished the Node, Express, Angular 7, GraphQL and MongoDB CRUD Web App. If you can’t follow the steps of the tutorial, you can compare it with the working source code from our GitHub.

Thanks for reading ❤

Создание сайта на Mongo DB, Express JS, Node JS и Angular

Создание сайта на Mongo DB, Express JS, Node JS и Angular

Видео курс по изучению стека MEAN. В курсе вы научитесь создавать сайты при помощи Node JS, Express JS, Angular JS и баз данных MongoDB. Вы ознакомитесь со всеми моментами разработки и в конце курса выгрузите сайт на удаленный сервер.

Видео курс по изучению стека MEAN. В курсе вы научитесь создавать сайты при помощи Node JS, Express JS, Angular JS и баз данных MongoDB. Вы ознакомитесь со всеми моментами разработки и в конце курса выгрузите сайт на удаленный сервер.

How to Drag and Drop File Uploading with MongoDB and Multer in Angular 8

How to Drag and Drop File Uploading with MongoDB and Multer in Angular 8

How to Drag and Drop File Uploading with MongoDB and Multer? we will learn to upload multiple image files in MongoDB database using Node and Express. In this tutorial we will create a basic Angular app in which we will create a custom directive to build Angular drag and drop functionality.

In this Angular 8 drag and drop file uploading tutorial, we will learn to upload multiple image files in MongoDB database using Node and Express. In this tutorial we will create a basic Angular app in which we will create a custom directive to build Angular drag and drop functionality.

Tutorial Objective

  • Building Angular drag and drop file uploading Layout with HTML/CSS
  • Creating a Node server to upload image files
  • Creating Custom Drag and Drop directive
  • Using Multer for Multiple file uploading
  • Multiple files uploading with progress bar
Install Angular App

Let’s start by installing basic Angular app, run the following command:

ng new angular-dragdrop-fileupload

Then, navigate to the newly created Angular project:

cd angular-dragdrop-fileupload

Next, create Angular component for drag and drop file upload.

ng g c drag-drop

Next, run command to install Bootstrap.

npm install bootstrap

Add the Bootstrap CSS in package.json file.

"styles": [
          "node_modules/bootstrap/dist/css/bootstrap.min.css",
          "src/styles.css"
         ]

Run command to start your Angular project.

ng serve --open
Build Node/Express Server

Build a node server with express js to store the uploaded files on the MongoDB database. We will use Multer to store the image files along with other NPM packages.

Run the command from Angular project’s root to generate backend folder:

mkdir backend && cd backend

Create separate package.json for node server.

npm init

Run command to install required NPM packages.

npm install body-parser cors express mongoose multer --save

Also, install nodemon NPM module, it starts the server whenever any change occurs in server code.

npm install nodemon --save-dev
Define MongoDB Database

Create database folder inside the backend folder and also create a file backend/database/db.js in it.

module.exports = {
  db: 'mongodb://localhost:27017/meanfileupload'
}
Define Mongoose Schema

Create models folder inside the backend directory, then create a file User.js and place the following code inside of it.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

// Define Schema
let userSchema = new Schema({
_id: mongoose.Schema.Types.ObjectId,
avatar: {
type: Array
},
}, {
collection: 'users'
})

module.exports = mongoose.model('User', userSchema)

Build File Upload REST API with Multer & Express

Let’s first create a folder and name it public inside the backend folder. Here, in this folder where we will store all the uploaded files.

Run the command from the backend folder’s root.

mkdir public

Create a routes folder inside the backend folder. Create a file user.routes.js inside of it. Here we ill import express, multer and mongoose NPM modules. By using these services we will build REST API for storing multiple files in MongoDB database.

Add the given below code inside the user.routes.js.

let express = require('express'),
multer = require('multer'),
mongoose = require('mongoose'),
router = express.Router();

// Multer File upload settings
const DIR = './public/';

const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, DIR);
},
filename: (req, file, cb) => {
const fileName = file.originalname.toLowerCase().split(' ').join('-');
cb(null, fileName)
}
});

var upload = multer({
storage: storage,
// limits: {
// fileSize: 1024 * 1024 * 5
// },
fileFilter: (req, file, cb) => {
if (file.mimetype == "image/png" || file.mimetype == "image/jpg" || file.mimetype == "image/jpeg") {
cb(null, true);
} else {
cb(null, false);
return cb(new Error('Only .png, .jpg and .jpeg format allowed!'));
}
}
});

// User model
let User = require('../models/User');

router.post('/create-user', upload.array('avatar', 6), (req, res, next) => {
const reqFiles = []
const url = req.protocol + '://' + req.get('host')
for (var i = 0; i < req.files.length; i++) {
reqFiles.push(url + '/public/' + req.files[i].filename)
}

const user = new User({
_id: new mongoose.Types.ObjectId(),
avatar: reqFiles
});
user.save().then(result => {
console.log(result);
res.status(201).json({
message: "Done upload!",
userCreated: {
_id: result._id,
avatar: result.avatar
}
})
}).catch(err => {
console.log(err),
res.status(500).json({
error: err
});
})
})

router.get("/", (req, res, next) => {
User.find().then(data => {
res.status(200).json({
message: "User list retrieved successfully!",
users: data
});
});
});

module.exports = router;

We used Multer’s upload.array() method to upload the multiple files on the server. This method takes 2 arguments, first we pass the file name which we will be using to store the file values. Second parameter relates to the number of file we can upload at a time. Then we defined the reqFiles array here we will store the uploaded file’s path with full URL.

Configure Node/Express Server

Create server.js file inside the backend folder. Then, place the following code inside the server.js file.

let express = require('express'),
mongoose = require('mongoose'),
cors = require('cors'),
bodyParser = require('body-parser'),
dbConfig = require('./database/db');

// Routes to Handle Request
const userRoute = require('../backend/routes/user.route')

// MongoDB Setup
mongoose.Promise = global.Promise;
mongoose.connect(dbConfig.db, {
useNewUrlParser: true
}).then(() => {
console.log('Database sucessfully connected')
},
error => {
console.log('Database could not be connected: ' + error)
}
)

// Setup Express.js
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: false
}));
app.use(cors());

// Make Images "Uploads" Folder Publicly Available
app.use('/public', express.static('public'));

// API Route
app.use('/api', userRoute)

const port = process.env.PORT || 4000;
const server = app.listen(port, () => {
console.log('Connected to port ' + port)
})

// Error
app.use((req, res, next) => {
// Error goes via next() method
setImmediate(() => {
next(new Error('Something went wrong'));
});
});

app.use(function (err, req, res, next) {
console.error(err.message);
if (!err.statusCode) err.statusCode = 500;
res.status(err.statusCode).send(err.message);
});

Start Node Server

Open terminal and run command to start the MongoDB server.

mongod

Then, open another terminal and run following command.

nodemon server.js

Next, you can checkout node server running on the following Url: http://localhost:4000/api

API MethodURLGET http://localhost:4000/api POST/api/create-user

You can test out Angular file uploading REST APIs Url in Postmen:

Create Angular 8 Drag and Drop File Uploading Directive

In this step, we will create HostBinding and HostListeners to manage the drag and drop functionality for Angular file upload task.

Run command to create directive in Angular project.

ng g d drag-drop-file-upload

In the drag-drop-file-upload.directive.ts file, we will define 3 HostListners such as DragoverDragleave and Drop along with HostBinding for background-color.

import { Directive, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';

@Directive({
selector: '[appDragDropFileUpload]'
})

export class DragDropFileUploadDirective {

@Output() fileDropped = new EventEmitter<any>();

@HostBinding('style.background-color') private background = '#ffffff';

// Dragover Event
@HostListener('dragover', ['$event']) dragOver(event) {
event.preventDefault();
event.stopPropagation();
this.background = '#e2eefd';
}

// Dragleave Event
@HostListener('dragleave', ['$event']) public dragLeave(event) {
event.preventDefault();
event.stopPropagation();
this.background = '#ffffff'
}

// Drop Event
@HostListener('drop', ['$event']) public drop(event) {
event.preventDefault();
event.stopPropagation();
this.background = '#ffffff';
const files = event.dataTransfer.files;
if (files.length > 0) {
this.fileDropped.emit(files)
}
}

}

Create Angular 8 Service

We need to create Angular service, here in this file we will create a method in which we will make HTTP POST request to store the uploaded files in the mongoDB database.

Use JavaScript’s FormData() method to store the Reactive Forms value in the database via Reactive Form. To track the file upload progress define the reportProgress and observe values in Http method.

import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { HttpErrorResponse, HttpClient } from '@angular/common/http';

@Injectable({
providedIn: 'root'
})

export class DragdropService {

constructor(private http: HttpClient) { }

addFiles(images: File) {
var arr = []
var formData = new FormData();
arr.push(images);

arr[0].forEach((item, i) =&gt; {
  formData.append('avatar', arr[0][i]);
})

return this.http.post('http://localhost:4000/api/create-user', formData, {
  reportProgress: true,
  observe: 'events'
}).pipe(
  catchError(this.errorMgmt)
)

}

errorMgmt(error: HttpErrorResponse) {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
// Get client-side error
errorMessage = error.error.message;
} else {
// Get server-side error
errorMessage = Error Code: ${error.status}\nMessage: ${error.message};
}
console.log(errorMessage);
return throwError(errorMessage);
}

}

Create Drag and Drop File Upload Component

Now, we will create the layout for drag and drop file upload component. In this tutorial we will be using Reactive Forms to store the files and Node server to store the files into the mongoDB database.

Import ReactiveFormsModule in app.module.ts file to enable the service.

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
declarations: [...],
imports: [
ReactiveFormsModule
],
bootstrap: [...]
})

export class AppModule { }

Next, add the code inside the app/drag-drop.component.html file.

<div class="container fileUploadWrapper">
<form [formGroup]="form">
<div class="row">
<!-- Progress Bar -->
<div class="col-md-12" *ngIf="progress">
<div class="progress form-group">
<div class="progress-bar progress-bar-striped bg-success" role="progressbar"
[style.width.%]="progress">
</div>
</div>
</div>

        &lt;div class="col-md-12"&gt;
            &lt;div class="fileupload" appDragDropFileUpload (click)="fileField.click()"
                (fileDropped)="upload($event)"&gt;
                &lt;span class="ddinfo"&gt;Choose a file or drag here&lt;/span&gt;
                &lt;input type="file" name="avatars" #fileField (change)="upload($event.target.files)" hidden multiple&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="col-md-12"&gt;
            &lt;div class="image-list" *ngFor="let file of fileArr; let i = index"&gt;
                &lt;div class="profile"&gt;
                    &lt;img [src]="sanitize(file['url'])" alt=""&gt;
                &lt;/div&gt;
                &lt;p&gt;{{file.item.name}}&lt;/p&gt;
            &lt;/div&gt;
            &lt;p class="message"&gt;{{msg}}&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/form&gt;

</div>

Apply design to Angular drag and drop file uploading component, navigate to styles.css and paste the following code.

* {
box-sizing: border-box;
}

body {
margin: 0;
padding: 25px 0 0 0;
background: #291464;
}

.container {
margin-top: 30px;
max-width: 500px;
}

.progress {
margin-bottom: 30px;
}

.fileupload {
background-image: url("./assets/upload-icon.png");
background-repeat: no-repeat;
background-size: 100px;
background-position: center;
background-color: #ffffff;
height: 200px;
width: 100%;
cursor: pointer;
/* border: 2px dashed #0f68ff; */
border-radius: 6px;
margin-bottom: 25px;
background-position: center 28px;
}

.ddinfo {
display: block;
text-align: center;
padding-top: 130px;
color: #a0a1a2;
}

.image-list {
display: flex;
width: 100%;
background: #C2DFFC;
border: 1px solid;
border-radius: 3px;
padding: 10px 10px 10px 15px;
margin-bottom: 10px;
}

.image-list p {
line-height: normal;
padding: 0;
margin: 0 0 0 14px;
display: inline-block;
position: relative;
top: -2px;
color: #150938;
font-size: 14px;
}

.message {
text-align: center;
color: #C2DFFC;
}

.remove {
background: transparent;
border: none;
cursor: pointer;
}

.profile {
width: 40px;
height: 40px;
overflow: hidden;
border-radius: 4px;
display: inline-block;
}

.profile img {
width: 100%;
}

.remove img {
width: 15px;
position: relative;
top: -2px;
}

.fileUploadWrapper .card-body {
max-height: 330px;
overflow: hidden;
overflow-y: auto;
}

@media(max-width: 767px) {
.container {
width: 280px;
margin: 20px auto 100px;
}
}

Paste the following code in app/drag-drop.component.ts file:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray } from "@angular/forms";
import { DragdropService } from "../dragdrop.service";
import { HttpEvent, HttpEventType } from '@angular/common/http';
import { DomSanitizer } from '@angular/platform-browser';

@Component({
selector: 'app-drag-drop',
templateUrl: './drag-drop.component.html',
styleUrls: ['./drag-drop.component.css']
})

export class DragDropComponent implements OnInit {

fileArr = [];
imgArr = [];
fileObj = [];
form: FormGroup;
msg: string;
progress: number = 0;

constructor(
public fb: FormBuilder,
private sanitizer: DomSanitizer,
public dragdropService: DragdropService
) {
this.form = this.fb.group({
avatar: [null]
})
}

ngOnInit() { }

upload(e) {
const fileListAsArray = Array.from(e);
fileListAsArray.forEach((item, i) => {
const file = (e as HTMLInputElement);
const url = URL.createObjectURL(file[i]);
this.imgArr.push(url);
this.fileArr.push({ item, url: url });
})

this.fileArr.forEach((item) =&gt; {
  this.fileObj.push(item.item)
})

// Set files form control
this.form.patchValue({
  avatar: this.fileObj
})

this.form.get('avatar').updateValueAndValidity()

// Upload to server
this.dragdropService.addFiles(this.form.value.avatar)
  .subscribe((event: HttpEvent&lt;any&gt;) =&gt; {
    switch (event.type) {
      case HttpEventType.Sent:
        console.log('Request has been made!');
        break;
      case HttpEventType.ResponseHeader:
        console.log('Response header has been received!');
        break;
      case HttpEventType.UploadProgress:
        this.progress = Math.round(event.loaded / event.total * 100);
        console.log(`Uploaded! ${this.progress}%`);
        break;
      case HttpEventType.Response:
        console.log('File uploaded successfully!', event.body);
        setTimeout(() =&gt; {
          this.progress = 0;
          this.fileArr = [];
          this.fileObj = [];
          this.msg = "File uploaded successfully!"
        }, 3000);
    }
  })

}

// Clean Url
sanitize(url: string) {
return this.sanitizer.bypassSecurityTrustUrl(url);
}

}

Conclusion

Finally, Angular 8 Drag and Drop multiple files uploading tutorial with MongoDB & Multer is over.

I hope this tutorial will surely help and you if you liked this tutorial, please consider sharing it with others.

This post was originally published here