MEAN Stack Angular 6 CRUD Web Application

by Didin J. on May 20, 2018 MEAN Stack Angular 6 CRUD Web Application

Step by step tutorial on building MEAN Stack (Angular 6) Create-Read-Update-Delete (CRUD) Web Application

The comprehensive step by step tutorial on building MEAN Stack (Angular 6) Create-Read-Update-Delete (CRUD) Web Application. In this MEAN Stack Angular 6 tutorial we will be focused on the new feature in Angular 6 that support CRUD operation. Start from RESTful API access to UI/UX using Angular 6 Material, it different than previous MEAN Stack tutorial.


Table of Contents:


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

  1. Node.js (recommended version)
  2. Angular CLI
  3. Angular 6
  4. MongoDB
  5. Express.js
  6. Mongoose.js
  7. Terminal (Mac/Linux) or Node Command Line (Windows)
  8. IDE or Text Editor

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.11.1
npm -v
6.0.0

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


Install Angular CLI and Create Angular 6 Web Application

To install or upgrade the latest Angular 6 CLI, type this command in the terminal or Node command line.

sudo npm install -g @angular/cli

If you use windows, it might be not necessary to add `sudo`. Next, create a new Angular 6 Web Application using this Angular CLI command.

ng new mean-angular6

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

cd ./mean-angular6

Now, run the new Angular 6 web application using this command.

ng serve

Open the browser then go to this address `localhost:4200` and you will see this page.

MEAN Stack Angular 6 CRUD Web Application - Angular Home


Install and Configure Express.js

Close the running Angular 6 app first by press `ctrl+c` then type this command for adding Express.js modules and its dependencies (express, body-parser, morgan, body-parser, serve-favicon, http-errors).

npm install --save express body-parser morgan body-parser serve-favicon http-errors

Then, add bin folder and a `www` file inside the `bin` folder.

mkdir bin
touch bin/www

Open and edit www file then add these lines of codes to configure HTTP web server.

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('mean-angular6:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

To make the server run from `bin/www`, open and edit `package.json` then replace `start` value.

"scripts": {
  "ng": "ng",
  "start": "ng build && node ./bin/www",
  "build": "ng build",
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
},

Now, create `app.js` in the root of the project folder.

touch app.js

Open and edit `app.js` then add all these lines of codes to configure initial requests and responses.

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');

var apiRouter = require('./routes/book');

var app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'dist/mean-angular6')));
app.use('/', express.static(path.join(__dirname, 'dist/mean-angular6')));
app.use('/api', apiRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.send(err.status);
});

module.exports = app;

Next, create routes folder then create routes file for the book.

mkdir routes
touch routes/book.js

Open and edit `routes/book.js` file then add these lines of codes of initial GET router with dummy text response.

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.send('Express RESTful API');
});

module.exports = router;

Now, run the server using this command.

npm start

You will see the previous Angular landing page when you point your browser to `http://localhost:3000`. When you change the address to `http://localhost:3000/api` you will see this page.

MEAN Stack Angular 6 CRUD Web Application - Express RESTful API

Now, we have REST API with the compiled Angular 6 front end.


Install and Configure Mongoose.js

We need to access data from MongoDB. For that, we will install and configure `Mongoose.js`. On the terminal type this command after stopping the running Express server.

npm install --save mongoose

Open and edit `app.js` then add these lines after another variable line.

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

Now, run the MongoDB server on different terminal tab or command line or run from the service.

mongod

Next, you can test the connection to MongoDB run again the Node application and you will see this message on the terminal.

connection successful


Create Mongoose.js Model

Add a models folder on the root of the project folder for hold `Mongoose.js` model files.

mkdir models

Create a new Javascript file that uses for `Mongoose.js` model. We will create a model of Book collection.

touch models/Book.js

Now, open and edit that file and add Mongoose require.

var mongoose = require('mongoose');

Then add model fields like this.

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

That Schema will mapping to MongoDB collections called book. If you want to know more about Mongoose Schema Datatypes you can find it here. Next, export that schema.

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


Create Routes for Accessing Book Data via REST API

Open and edit again `routes/book.js` then replace all codes with this CRUD GET, POST, PUT, and DELETE router.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Book = require('../models/Book.js');

/* GET ALL BOOKS */
router.get('/', function(req, res, next) {
  Book.find(function (err, products) {
    if (err) return next(err);
    res.json(products);
  });
});

/* GET SINGLE BOOK BY ID */
router.get('/:id', function(req, res, next) {
  Book.findById(req.params.id, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* SAVE BOOK */
router.post('/', function(req, res, next) {
  Book.create(req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* UPDATE BOOK */
router.put('/:id', function(req, res, next) {
  Book.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* DELETE BOOK */
router.delete('/:id', function(req, res, next) {
  Book.findByIdAndRemove(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

module.exports = router;

Run again the Express server then open the other terminal or command line to test the Restful API by type this command.

curl -i -H "Accept: application/json" localhost:3000/api

If that command return response like below then REST API is ready to go.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
Date: Fri, 18 May 2018 13:52:20 GMT
Connection: keep-alive

[]

Now, let's populate Book collection with initial data sent from REST API. Run this command to populate it.

curl -i -X POST -H "Content-Type: application/json" -d '{ "isbn":"123442123, 97885654453443","title":"Learn how to build modern web application with MEAN stack","author": "Didin J.","description":"The comprehensive step by step tutorial on how to build MEAN (MongoDB, Express.js, Angular 5 and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com" }' localhost:3000/api

You will see this response to the terminal if success.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 415
ETag: W/"19f-OBSCF1PZfqMcJVrpMPdOa/K9GAs"
Date: Fri, 18 May 2018 13:53:17 GMT
Connection: keep-alive

{"_id":"5afedacdafd63f4ad09766c6","isbn":"123442123, 97885654453443","title":"Learn how to build modern web application with MEAN stack","author":"Didin J.","description":"The comprehensive step by step tutorial on how to build MEAN (MongoDB, Express.js, Angular 5 and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com","updated_date":"2018-05-18T13:53:17.348Z","__v":0}


Create Angular 6 Routes for Navigation between Angular Pages/Component

To create Angular 6 Routes for navigation between Angular 6 pages/component, add or generate all required component.

ng g component book
ng g component book-detail
ng g component book-create
ng g component book-edit

Next, open and edit again `src/app/app.module.ts` then add this import of RouterModule and Routes.

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

Add these lines of codes for Angular routes before `@NgModule`. These routes register all required components or pages.

const appRoutes: Routes = [
  {
    path: 'books',
    component: BookComponent,
    data: { title: 'Book List' }
  },
  {
    path: 'book-details/:id',
    component: BookDetailComponent,
    data: { title: 'Book Details' }
  },
  {
    path: 'book-create',
    component: BookCreateComponent,
    data: { title: 'Create Book' }
  },
  {
    path: 'book-edit/:id',
    component: BookEditComponent,
    data: { title: 'Edit Book' }
  },
  { path: '',
    redirectTo: '/books',
    pathMatch: 'full'
  }
];

In @NgModule imports, section adds ROUTES constant, so the imports section will be like this.

imports: [
  RouterModule.forRoot(appRoutes),
  BrowserModule,
  FormsModule,
  HttpClientModule
],

To activate that routes in Angular 6, open and edit `src/app/app.component.html` then replace all codes with this <router-outlet>.

<router-outlet></router-outlet>


Create Angular 6 Service for Accessing REST API

Before creating a service for RESTful API access, first, we have to install or register `HttpClientModule`. Open and edit `src/app/app.module.ts` then add these imports of FormsModule and HttpClientModule.

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

Add it to `@NgModule` imports after `BrowserModule`.

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule
],

To create an Angular 6 service, simply run this command.

ng g service api

Next, open and edit `src/api.service.ts` then add these imports of RxJS Observable, of, throwError, Angular HttpClient, HttpHeaders, HttpErrorResponse, RxJS Operators catchError, tap, and map.

import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';

Add these constants before the `@Injectable`.

const httpOptions = {
  headers: new HttpHeaders({'Content-Type': 'application/json'})
};
const apiUrl = "/api";

Inject `HttpClient` module to the constructor.

constructor(private http: HttpClient) { }

Add the error handler function.

private handleError(error: HttpErrorResponse) {
  if (error.error instanceof ErrorEvent) {
    // A client-side or network error occurred. Handle it accordingly.
    console.error('An error occurred:', error.error.message);
  } else {
    // The backend returned an unsuccessful response code.
    // The response body may contain clues as to what went wrong,
    console.error(
      `Backend returned code ${error.status}, ` +
      `body was: ${error.error}`);
  }
  // return an observable with a user-facing error message
  return throwError('Something bad happened; please try again later.');
};

Add a function for extract response data.

private extractData(res: Response) {
  let body = res;
  return body || { };
}

Add all CRUD (create, read, update, delete) functions of books data.

getBooks(): Observable<any> {
  return this.http.get(apiUrl, httpOptions).pipe(
    map(this.extractData),
    catchError(this.handleError));
}

getBook(id: string): Observable<any> {
  const url = `${apiUrl}/${id}`;
  return this.http.get(url, httpOptions).pipe(
    map(this.extractData),
    catchError(this.handleError));
}

postBook(data): Observable<any> {
  return this.http.post(apiUrl, data, httpOptions)
    .pipe(
      catchError(this.handleError)
    );
}

updateBook(data): Observable<any> {
  return this.http.put(apiUrl, data, httpOptions)
    .pipe(
      catchError(this.handleError)
    );
}

deleteBook(id: string): Observable<{}> {
  const url = `${apiUrl}/${id}`;
  return this.http.delete(url, httpOptions)
    .pipe(
      catchError(this.handleError)
    );
}


Display Book List using Angular 6 Component and Material

We will display a book list that gets via API Service. For that, open and edit `src/app/book/book.component.ts` then add this import of ApiService.

import { ApiService } from '../api.service';

Next, inject the API Service to the constructor.

constructor(private api: ApiService) { }

Add a variable for hold books list before the constructor.

books: any;

Modify the `ngOnInit` function to get the book list immediately.

ngOnInit() {
  this.api.getBooks()
    .subscribe(res => {
      console.log(res);
      this.books = res;
    }, err => {
      console.log(err);
    });
}

Next, for the user interface (UI) we will use Angular 6 Material. There's a new feature for generating a Material component like Table as a component, but we will create or add the Table component from scratch to existing component. Type this command to install Angular Material.

ng add @angular/material

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

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

Also, modify `FormsModule` import to add `ReactiveFormsModule`.

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

Register the above modules to `@NgModule` imports.

imports: [
  RouterModule.forRoot(appRoutes),
  BrowserModule,
  FormsModule,
  ReactiveFormsModule,
  HttpClientModule,
  BrowserAnimationsModule,
  MatInputModule,
  MatTableModule,
  MatPaginatorModule,
  MatSortModule,
  MatProgressSpinnerModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, back to `src/book/book.component.ts` then add these imports of CDK DataSource and RxJS Observable.

import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs';

Declare the variables of Angular Material Table Data Source after the books variable.

displayedColumns = ['isbn', 'title', 'author'];
dataSource = new BookDataSource(this.api);

Add a class for the Book Data Source.

export class BookDataSource extends DataSource<any> {
  constructor(private api: ApiService) {
    super()
  }

  connect() {
    return this.api.getBooks();
  }

  disconnect() {

  }
}

Next, open and edit `src/book/book.component.html` then replace all HTML tags with this Angular Material tags.

<div class="button-row">
  <a mat-raised-button color="primary" [routerLink]="['/book-create']"><mat-icon>add</mat-icon></a>
</div>
<div class="example-container mat-elevation-z8">
  <table mat-table #table [dataSource]="dataSource">

    <!--- Note that these columns can be defined in any order.
          The actual rendered columns are set as a property on the row definition" -->

    <!-- Title Column -->
    <ng-container matColumnDef="isbn">
      <th mat-header-cell *matHeaderCellDef> ISBN </th>
      <td mat-cell *matCellDef="let element" class="isbn-col"> {{element.isbn}} </td>
    </ng-container>

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

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

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

Finally, to make a little UI adjustment, open and edit `src/book/book.component.css` then add this CSS codes.

.example-container {
  display: flex;
  flex-direction: column;
  max-height: 500px;
  min-width: 300px;
  overflow: auto;
}

.isbn-col {
  flex: 0 0 100px !important;
  white-space: unset !important;
}

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


Show Book Details using Angular 6 Component and Material

To show book details after click or tap on the one of a row inside the Angular Material table, open and edit `src/book-detail/book-detail.component.ts` then add these imports of ActivatedRoute and ApiService.

import { ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';

Inject above modules to the constructor.

constructor(private route: ActivatedRoute, private api: ApiService) { }

Declare a variable before the constructor for hold book data that get from the API.

book = {};

Add a function for getting Book data from the API.

getBookDetails(id) {
  this.api.getBook(id)
    .subscribe(data => {
      console.log(data);
      this.book = data;
    });
}

Call that function when the component is initiated.

ngOnInit() {
  this.getBookDetails(this.route.snapshot.params['id']);
}

For the view, open and edit `src/book-detail/book-detail.component.html` then replace all HTML tags with this.

<div class="button-row">
  <a mat-raised-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.description}}</mat-card-subtitle>
  </mat-card-header>
  <mat-card-content>
    <dl>
      <dt>ISBN:</dt>
      <dd>{{book.isbn}}</dd>
      <dt>Author:</dt>
      <dd>{{book.author}}</dd>
      <dt>Publisher:</dt>
      <dd>{{book.publisher}}</dd>
      <dt>Publish Year:</dt>
      <dd>{{book.published_year}}</dd>
      <dt>Update Date:</dt>
      <dd>{{book.updated_date | date}}</dd>
    </dl>
  </mat-card-content>
  <mat-card-actions>
    <a mat-raised-button color="primary" [routerLink]="['/book-edit', book._id]"><mat-icon>edit</mat-icon></a>
    <a mat-raised-button color="warn" (click)="deleteBook(book._id)"><mat-icon>delete</mat-icon></a>
  </mat-card-actions>
</mat-card>

Finally, open and edit `src/book-detail/book-detail.component.css` then add these lines of CSS codes.

.example-card {
  max-width: 500px;
}

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


Add a Book using Angular 6 Component and Material

To create a form for adding a Book, open and edit `src/book-create/book-create.component.ts` then add these imports of Angular Router, ApiService, Angular FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, and Validators.

import { Router } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';

Inject above modules to the constructor.

constructor(private router: Router, private api: ApiService, private formBuilder: FormBuilder) { }

Declare variables for the Form Group and all of the required fields inside the form before the constructor.

bookForm: FormGroup;
isbn:string='';
title:string='';
description:string='';
author:string='';
publisher:string='';
published_year:string='';

Add initial validation for each field.

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

Create a function for submitting or POST book form.

onFormSubmit(form:NgForm) {
  this.api.postBook(form)
    .subscribe(res => {
        let id = res['_id'];
        this.router.navigate(['/book-details', id]);
      }, (err) => {
        console.log(err);
      });
}

Next, open and edit `src/book-create/book-create.component.html` then replace all HTML tags with this.

<div class="button-row">
  <a mat-raised-button color="primary" [routerLink]="['/books']"><mat-icon>list</mat-icon></a>
</div>
<form [formGroup]="bookForm" (ngSubmit)="onFormSubmit(bookForm.value)">
  <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 Book 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 Book 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 Book 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" formControlName="published_year"
           [errorStateMatcher]="matcher">
    <mat-error>
      <span *ngIf="!bookForm.get('published_year').valid && bookForm.get('published_year').touched">Please enter Published Year</span>
    </mat-error>
  </mat-form-field>
  <div class="button-row">
    <button type="submit" [disabled]="!bookForm.valid" mat-raised-button color="primary"><mat-icon>save</mat-icon></button>
  </div>
</form>

Finally, open and edit `src/book-create/book-create.component.css` then add these CSS codes.

.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;
}


Edit a Book using Angular 6 Component and Material

We have put an edit button inside the Book Detail component. Now, open and edit `src/book-edit/book-edit.component.ts` then add these imports of Angular Router, ApiService, Angular FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, and Validators.

import { Router, ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';

Inject above modules to the constructor.

constructor(private router: Router, private route: ActivatedRoute, private api: ApiService, private formBuilder: FormBuilder) { }

Declare the Form Group variable and all of the required variables for the FormGroup of the book before the constructor.

bookForm: FormGroup;
id:string = '';
isbn:string = '';
title:string = '';
description:string = '';
author:string = '';
publisher:string = '';
published_year:string = '';

Next, add validation for all fields when the component is initiated.

ngOnInit() {
  this.getBook(this.route.snapshot.params['id']);
  this.bookForm = this.formBuilder.group({
    'isbn' : [null, Validators.required],
    'title' : [null, Validators.required],
    'description' : [null, Validators.required],
    'author' : [null, Validators.required],
    'publisher' : [null, Validators.required],
    'published_year' : [null, Validators.required]
  });
}

Create a function for getting book data that filled to each form fields.

getBook(id) {
  this.api.getBook(id).subscribe(data => {
    this.id = data._id;
    this.bookForm.setValue({
      isbn: data.isbn,
      title: data.title,
      description: data.description,
      author: data.author,
      publisher: data.publisher,
      published_year: data.published_year
    });
  });
}

Create a function to update the book changes.

onFormSubmit(form:NgForm) {
  this.api.updateBook(this.id, form)
    .subscribe(res => {
        let id = res['_id'];
        this.router.navigate(['/book-details', id]);
      }, (err) => {
        console.log(err);
      }
    );
}

Add a function for handling the show book details button.

bookDetails() {
  this.router.navigate(['/book-details', this.id]);
}

Next, open and edit `src/book-edit/book-edit.component.html` then replace all HTML tags with this.

<div class="button-row">
  <a mat-raised-button color="primary" (click)="bookDetails()"><mat-icon>show</mat-icon></a>
</div>
<form [formGroup]="bookForm" (ngSubmit)="onFormSubmit(bookForm.value)">
  <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 Book 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 Book 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 Book 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" formControlName="published_year"
           [errorStateMatcher]="matcher">
    <mat-error>
      <span *ngIf="!bookForm.get('published_year').valid && bookForm.get('published_year').touched">Please enter Published Year</span>
    </mat-error>
  </mat-form-field>
  <div class="button-row">
    <button type="submit" [disabled]="!bookForm.valid" mat-raised-button color="primary"><mat-icon>save</mat-icon></button>
  </div>
</form>

Finally, open and edit `src/book-edit/book-edit.component.css` then add these lines of CSS codes.

.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;
}


Create a Function for Delete Book inside Book Detail Component

Inside book detail component, you have seen there a button for delete and have click action to a function. Now, open and edit `src/book-detail/book-detail.ts` then modify the `ActivatedRoute` import to add `Router` module.

import { ActivatedRoute, Router } from '@angular/router';

Inject `Router` to the constructor.

constructor(private route: ActivatedRoute, private api: ApiService, private router: Router) { }

Add this function for delete book.

deleteBook(id) {
  this.api.deleteBook(id)
    .subscribe(res => {
        this.router.navigate(['/books']);
      }, (err) => {
        console.log(err);
      }
    );
}


Run and Test the MEAN Stack (Angular 6) CRUD Application

Now, it's a time for testing the MEAN Stack (Angular 6) CRUD Application.

npm start

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

MEAN Stack Angular 6 CRUD Web Application - Book List
MEAN Stack Angular 6 CRUD Web Application - Add Book
MEAN Stack Angular 6 CRUD Web Application - Book Details

That it's, we have finished the complete tutorial of MEAN Stack (Angular 6) and Angular Material. If you can't follow the steps of the tutorial, you can compare it with the working source code from our GitHub.

If you don’t want to waste your time design your own front-end or your budget to spend by hiring a web designer then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:

Thanks!

Loading…