MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly

by Didin J. on Feb 22, 2020 MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly

A comprehensive step by step tutorial on build realtime CRUD web abb quickly using MEAN Stack with the latest Angular 9

A comprehensive step by step tutorial means starting to build complete MEAN stack Angular 9 realtime CRUD web app from scratch to fully functional web app. Realtime CRUD web app is the create, read, update, and delete operation in realtime response. Every change or new data that created will automatically read by the client without reloading or refresh the browser or re-initiate component in Angular 9. This time we will build a data entry for item sales which response as a realtime list and pie chart.

This tutorial divided into several steps:

As you see in the steps list, we will use Socket.io for realtime operation in both server and client. Also, using Ng2Charts and Chart.js to display a pie chart on the list page. The following tools, frameworks, modules, and libraries are required for this tutorial:

  1. Node.js
  2. MongoDB
  3. Angular 8
  4. Angular CLI
  5. Express.js
  6. Mongoose.js
  7. Socket.io
  8. Ng2Charts
  9. Chart.js
  10. Terminal or Command Line
  11. IDE or Text Editor (we are using VSCode)

Before the move to the main steps of this tutorial, make sure that you have installed Node.js and MongoDB on your machine. You can check the Node.js version after installing it from the terminal or Node.js command line.

node -v
v12.16.1
npm -v
6.13.7

You can watch the video tutorial on our YouTube channel. Please like, share, comment and subscribe to our channel.


Step #1: New Node Express.js App using Express Generator

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. To create the Express.js app, we will be using the Express generator. Type this command to install it.

sudo npm install -g express-generator

Next, create an Express.js app by typing this command.

express sales-report --no-view

Go to the newly created sales-report folder then install all NPM modules.

cd ./sales-report
npm install

Now, we have this Express.js app structure for the sales-report app.

.
|-- app.js
|-- bin
|   `-- www
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
|   |-- images
|   |-- index.html
|   |-- javascripts
|   `-- stylesheets
|       `-- style.css
`-- routes
    |-- index.js
    `-- users.js


To check and sanitize the Express.js app, run this app for the first time.

nodemon

or

npm start

Then you will see this page when open the browser and go to `localhost:3000`.

MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly - Express welcome

To make this Express.js server accessible from the different port or domain. Enable the CORS by adding this module.

npm i --save cors

Next, add this require after other `require`.

var cors = require('cors');

Then add this line to `app.js` before other `app.use`.

app.use(cors());


Step #2: Install Mongoose.js and Socket.io

We will use Mongoose as the ODM for MongoDB. For realtime event emitter, we are using Socket.io. To install those required modules, type this command.

npm install --save mongoose socketio socket.io-client 

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

var mongoose = require('mongoose');

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

mongoose.connect('mongodb://localhost/blog-cms', {
    promiseLibrary: require('bluebird'),
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: 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.


Step #3: Add Mongoose Models or Schemas

We will use MongoDB collections for Sales Report. For that, we need to create new Mongoose models or schemas for it. First, create a new folder in the root of the project folder that holds the Mongoose models or schemas files then add this model file.

mkdir models
touch models/Sales.js

Open and edit `models/Sales.js` then add these lines of Mongoose schema.

var mongoose = require('mongoose');

var SalesSchema = new mongoose.Schema({
  id: String,
  itemId: String,
  itemName: String,
  itemPrice: Number,
  itemQty: Number,
  toralPrice: String,
  updated: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Sales', SalesSchema);


Step #4: Add Express and Socket.io Real-time Router

We call this Express router as the realtime router because every new data posted the Socket.io will emit that new data to the client then the client will get the list of data. First, create a new file inside the `routes` folder.

touch routes/sales.js

Open and edit `routes/sales.js` then add these imports.

var express = require('express');
var router = express.Router();
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var Sales = require('../models/Sales.js');

Add a server port for Socket.io to listen to requests from the client.

server.listen(4000)

Add these implementations of the Socket.io in the Express router.

// socket io
io.on('connection', function (socket) {
    socket.on('newdata', function (data) {
        io.emit('new-data', { data: data });
    });
    socket.on('updatedata', function (data) {
      io.emit('update-data', { data: data });
    });
});

Add a router to get a list of data from MongoDB collection.

// list data
router.get('/', function(req, res) {
    Sales.find(function (err, sales) {
        if (err) return next(err);
        res.json(sales);
    });
});

Add a router to get the sales report data from MongoDB collection.

// item sales report
router.get('/itemsales',  function(req, res, next) {
    Sales.aggregate([
        {
            $group: { 
                _id: { itemId: '$itemId', itemName: '$itemName' }, 
                totalPrice: {
                    $sum: '$totalPrice'
                }
            }
        },
        { $sort: {totalPrice: 1} }
    ], function (err, sales) {
        if (err) return next(err);
        res.json(sales);
    });
});

Add other routers of CRUD operational.

// get data by id
router.get('/:id', function(req, res, next) {
    Sales.findById(req.params.id, function (err, sales) {
        if (err) return next(err);
        res.json(sales);
    });
});
  
// post data
router.post('/', function(req, res, next) {
    Sales.create(req.body, function (err, sales) {
        if (err) {
            console.log(err);
            return next(err);
        }
        res.json(sales);
    });
});
  
// put data
router.put('/:id', function(req, res, next) {
    Sales.findByIdAndUpdate(req.params.id, req.body, function (err, sales) {
        if (err) {
            console.log(err);
            return next(err);
        }
        res.json(sales);
    });
});
  
// delete data by id
router.delete('/:id', function(req, res, next) {
    Sales.findByIdAndRemove(req.params.id, req.body, function (err, sales) {
        if (err) return next(err);
        res.json(sales);
    });
});

Run again the MongoDB and Express server then open a new terminal tab or command line to test the Restful API using the CURL command. To check to get the list of data types this CURL command.

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

That command should show the response of the empty JSON array.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 8142
ETag: W/"1fce-/FHcYzQEtYsn2ig1EPBAVYQ+2eE"
Date: Fri, 21 Feb 2020 02:19:15 GMT
Connection: keep-alive

[]

Now, let's post a data to the REST API using this command.

curl -i -X POST -H "Content-Type: application/json" -d '{ "itemId": "10011", "itemName": "Nokia 8", "itemPrice": 388, "itemQty": 5, "totalPrice": 1940 }' localhost:3000/api

That command should show a response like this.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 165
ETag: W/"a5-ssGlVsvJfkHmQ/nh8cZsfE2Yca4"
Date: Fri, 21 Feb 2020 02:25:24 GMT
Connection: keep-alive

{"_id":"5e4f3f94f1bcd5268893a5d8","itemId":"10011","itemName":"Nokia 8","itemPrice":388,"itemQty":5,"totalPrice":"1940","updated":"2020-02-21T02:25:24.705Z","__v":0}


Step #5: Create a New Angular 9 App

We will use Angular 9 CLI to install the Angular 9 app. Just type this command to install or update the Angular 9 CLI.

sudo npm install -g @angular/cli

Still in this project folder, create a new Angular 9 app by running this command.

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   [ https://sass-lang.com/
documentation/syntax#scss                ]

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

cd client

Type this command to run the Angular 9 app for the first time.

ng serve --open

Using the "--open" parameter will automatically open this Angular 9 app in the default browser. Now, the Angular initial app looks like this.

MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly - Angular Welcome


Step #6: Add Angular 9 Routing and Navigation

In the previous step, we have added Angular routing for this app. Next, we just add the required Angular components for this sales-report app. Just type these commands to generate them.

ng g component sales
ng g component sales-details
ng g component add-sales
ng g component edit-sales

We don't need to add or register those components to the app.module.ts because it already added automatically. Next, open and edit `src/app/app-routing.module.ts` then add these imports.

import { SalesComponent } from './sales/sales.component';
import { SalesDetailsComponent } from './sales-details/sales-details.component';
import { AddSalesComponent } from './add-sales/add-sales.component';
import { EditSalesComponent } from './edit-sales/edit-sales.component';

Add these arrays to the existing routes constant that contain route for above-added components.

const routes: Routes = [
  {
    path: 'sales',
    component: SalesComponent,
    data: { title: 'List of Sales' }
  },
  {
    path: 'sales-details/:id',
    component: SalesDetailsComponent,
    data: { title: 'Sales Details' }
  },
  {
    path: 'add-sales',
    component: AddSalesComponent,
    data: { title: 'Add Sales' }
  },
  {
    path: 'edit-sales/:id',
    component: EditSalesComponent,
    data: { title: 'Edit Sales' }
  },
  { path: '',
    redirectTo: '/sales',
    pathMatch: 'full'
  }
];

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

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

Open and edit `src/app/app.component.scss` then replace all SASS codes with this.

.container {
  padding: 20px;
}

 

Step #7: Add Angular 9 Service

We use Angular 9 service to access the Node-Express REST API for all CRUD operations. The response from the REST API emitted by Observable that can subscribe and read from the Components. Before creating a service for REST API access, first, we have to install or register `HttpClientModule`. Open and edit `src/app/app.module.ts` then add these imports of FormsModule, ReactiveFormsModule (@angular/forms) and HttpClientModule (@angular/common/http).

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

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

imports: [
  BrowserModule,
  FormsModule,
  ReactiveFormsModule,
  HttpClientModule,
  AppRoutingModule
],

We will use the type specifier to get a typed result object. For that, create a new Typescript file `src/app/sales.ts` then add these lines of Typescript codes.

export class Sales {
  id: string;
  itemId: string;
  itemName: string;
  itemPrice: number;
  itemQty: number;
  totalPrice: number;
  updated: Date;
}

And create a new Typescript file `src/app/chart.ts` then add these lines of Typescript codes.

export class Chart {
  id: any;
  totalPrice: number;
}

Next, generate an Angular 9 service by typing this command.

ng g service api

Next, open and edit `src/app/api.service.ts` then add these imports.

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

Add these constants before the `@Injectable`.

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

Inject the `HttpClient` module to the constructor.

  constructor(private http: HttpClient) { }

Add the error handler function that returns as an Observable.

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(error);
      return of(result as T);
    };
  }

Add the functions for all CRUD (create, read, update, delete) REST API call of sales and chart data. 

  getSales(): Observable<Sales[]> {
    return this.http.get<Sales[]>(`${apiUrl}`)
      .pipe(
        tap(sales => console.log('fetched sales')),
        catchError(this.handleError('getSales', []))
      );
  }

  getSalesById(id: string): Observable<Sales> {
    const url = `${apiUrl}/${id}`;
    return this.http.get<Sales>(url).pipe(
      tap(_ => console.log(`fetched sales id=${id}`)),
      catchError(this.handleError<Sales>(`getSalesById id=${id}`))
    );
  }

  addSales(sales: Sales): Observable<Sales> {
    return this.http.post<Sales>(apiUrl, sales, httpOptions).pipe(
      tap((s: Sales) => console.log(`added sales w/ id=${s._id}`)),
      catchError(this.handleError<Sales>('addSales'))
    );
  }

  updateSales(id: string, sales: Sales): Observable<any> {
    const url = `${apiUrl}/${id}`;
    return this.http.put(url, sales, httpOptions).pipe(
      tap(_ => console.log(`updated sales id=${id}`)),
      catchError(this.handleError<any>('updateSales'))
    );
  }

  deleteSales(id: string): Observable<Sales> {
    const url = `${apiUrl}/${id}`;
    return this.http.delete<Sales>(url, httpOptions).pipe(
      tap(_ => console.log(`deleted sales id=${id}`)),
      catchError(this.handleError<Sales>('deleteSales'))
    );
  }

  getChart(): Observable<Chart> {
    const url = `${apiUrl}/itemsales`;
    return this.http.get<Chart>(url).pipe(
      tap(_ => console.log(`fetched chart data`)),
      catchError(this.handleError<Chart>(`getChart data`))
    );
  }

You can find more examples of Angular 9 Observable and RXJS here.


Step #8: Show Realtime Angular Material List and Pie Chart

We will show a realtime Angular Material list on one page with the pie chart. For that, we need to prepare Angular Material, Ng2Charts, and Chart.js. Type this Angular schematic command to install Angular Material (@angular/material) in the client folder.

ng add @angular/material

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

? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: http
s://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes

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

import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';

Register the above modules to `@NgModule` imports.

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

Next, install Ng2Charts (ng2-charts), Chart.js and @types/socket.io-client by type this command.

npm i --save ng2-charts chart.js chartjs-plugin-datalabels @types/socket.io-client

Next, back to `src/app/app.module.ts` then add this import of ng2-charts.

import { ChartsModule } from 'ng2-charts';

Add this module to the @NgModule imports.

  imports: [
    ...
    ChartsModule
  ],

To make socket-io.client working with Angular 9 and Typescript, open and edit `client/src/polyfills.ts` then add this line at the bottom of the file.

(window as any).global = window;

Next, open and edit `src/app/sales/sales.component.ts` then add these imports.

import * as io from 'socket.io-client';
import { ChartType, ChartOptions } from 'chart.js';
import { Label } from 'ng2-charts';
import { ApiService } from '../api.service';
import { Sales } from '../sales';
import { Chart } from '../chart';

Declare the socket.io variable that listens to port 4000.

socket = io('http://localhost:4000');

Declare Chart data that hold the array of chart data from the REST API response.

chartData: Chart[] = [];

Declare the required Pie Chart elements as the variables;

  public pieChartOptions: ChartOptions = {
    responsive: true,
    legend: {
      position: 'top',
    },
    plugins: {
      datalabels: {
        formatter: (value, ctx) => {
          const label = ctx.chart.data.labels[ctx.dataIndex];
          return label;
        },
      },
    }
  };
  public pieChartLabels: Label[] = [];
  public pieChartData: number[] = [];
  public pieChartType: ChartType = 'pie';
  public pieChartLegend = true;
  public pieChartPlugins = [pluginDataLabels];
  public pieChartColors = [];

Declare the required variables to build an Angular Material table.

  displayedColumns: string[] = ['itemId', 'itemName', 'totalPrice'];
  data: Sales[] = [];
  isLoadingResults = true;

Inject the Angular ApiService to the constructor.

constructor(private api: ApiService) { }

Add a function to get sales data from the REST API.

  getSales() {
    this.api.getSales()
    .subscribe((res: any) => {
      this.data = res;
      console.log(this.data);
      this.isLoadingResults = false;
    }, err => {
      console.log(err);
      this.isLoadingResults = false;
    });
  }

Add a function to get chart data from the REST API then implement it to the ng2-charts/Chart.js pie chart.

  getChartData() {
    this.api.getChart()
    .subscribe((res: any) => {
      console.log(res);
      this.chartData = res;
      this.pieChartLabels = [];
      this.pieChartData = [];
      this.pieChartColors = [];
      const backgrounds = [];
      this.chartData.forEach((ch, idx) => {
        this.pieChartLabels.push(ch._id.itemName);
        this.pieChartData.push(ch.totalPrice);
        backgrounds.push(`rgba(${0 + (idx * 10)}, ${255 - (idx * 20)}, ${0 + (idx * 10)}, 0.3)`);
      });
      this.pieChartColors = [
        {
          backgroundColor: backgrounds
        }
      ];
    }, err => {
      console.log(err);
    });
  }

Call those functions inside Angular 9 NgOnInit function including the socket.io listener that listens the data change the reload the sales and chart data.

  ngOnInit(): void {
    this.getSales();
    this.getChartData();

    this.socket.on('update-data', function(data: any) {
      this.getSales();
      this.getChartData();
    }.bind(this));
  } 

Next, open and edit `src/app/sales/sales.component.html` then replace all HTML tags with these.

<div class="example-container mat-elevation-z8">
  <h2>Sales List</h2>
  <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]="['/add-sales']"><mat-icon>add</mat-icon> Sales</a>
  </div>
  <div class="top-charts">
    <div class="chart">
      <canvas baseChart
        [data]="pieChartData"
        [labels]="pieChartLabels"
        [chartType]="pieChartType"
        [options]="pieChartOptions"
        [plugins]="pieChartPlugins"
        [colors]="pieChartColors"
        [legend]="pieChartLegend">
      </canvas>
    </div>
  </div>
  <div class="mat-elevation-z8">
    <table mat-table [dataSource]="data" class="example-table"
           matSort matSortActive="itemName" matSortDisableClear matSortDirection="asc">

      <!-- Item ID Column -->
      <ng-container matColumnDef="itemId">
        <th mat-header-cell *matHeaderCellDef>Item ID</th>
        <td mat-cell *matCellDef="let row">{{row.itemId}}</td>
      </ng-container>

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

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

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

As you see, we put the ng2-charts/Chart.js pie chart a the top of the sales table. Next, open and edit `src/app/sales/sales.component.scss` the add these 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;
}

.top-charts {
  margin: 20px;
  padding: 20px;
}


Step #9: Show Realtime Details using Angular Material

We will show realtime data details using Angular Material and Socket.io. This component listens the Socket.io for any data change then reloads the data. Also, we will add a button to delete data that emit the update status to the Socket.io after successfully delete. Next, open and edit `src/app/sales-details/sales-details.component.ts` then add these lines of imports. 

import * as io from 'socket.io-client';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Sales } from './../sales';

Inject the above modules to the constructor.

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

Declare the variables before the constructor for hold sales data that get from the API and socket.io variable that listen to the port 4000.

  socket = io('http://localhost:4000');

  sales: Sales = { _id: '', itemId: '', itemName: '', itemPrice: null, itemQty: null, totalPrice: null, updated: null };
  isLoadingResults = true;

Add a function for getting sales data from the API.

  getSalesDetails(id: string) {
    this.api.getSalesById(id)
      .subscribe((data: any) => {
        this.sales = data;
        console.log(this.sales);
        this.isLoadingResults = false;
      });
  }

Call that function when the component is initiated. Also, add the socket.io listener to listen to the data changes.

  ngOnInit(): void {
    this.getSalesDetails(this.route.snapshot.params.id);

    this.socket.on('update-data', function(data: any) {
      this.getSalesDetails();
    }.bind(this));
  }

Add this function to delete a sales then emit a message to the socket.io.

  deleteSales(id: any) {
    this.isLoadingResults = true;
    this.api.deleteSales(id)
      .subscribe(res => {
          this.isLoadingResults = false;
          this.router.navigate(['/']);
          this.socket.emit('updatedata', res);
        }, (err) => {
          console.log(err);
          this.isLoadingResults = false;
        }
      );
  }

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

<div class="example-container mat-elevation-z8">
  <h2>Sales Details</h2>
  <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]="['/']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <mat-card-header>
      <mat-card-title><h2>{{sales.itemName}}</h2></mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <dl>
        <dt>Item ID:</dt>
        <dd>{{sales.itemId}}</dd>
        <dt>Price:</dt>
        <dd>{{sales.itemPrice}}</dd>
        <dt>Qty:</dt>
        <dd>{{sales.itemQty}}</dd>
        <dt>Total Price:</dt>
        <dd>{{sales.totalPrice}}</dd>
        <dt>Update Date:</dt>
        <dd>{{sales.updated | date}}</dd>
      </dl>
    </mat-card-content>
    <mat-card-actions>
      <a mat-flat-button color="primary" [routerLink]="['/edit-sales', sales._id]"><mat-icon>edit</mat-icon> Sales</a>
      <a mat-flat-button color="warn" (click)="deleteSales(sales._id)"><mat-icon>delete</mat-icon> Sales</a>
    </mat-card-actions>
  </mat-card>
</div>

Finally, open and edit `src/app/sales-details/sales-details.component.scss` then add this 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;
}


Step #10: Create Data using Angular Material Reactive Forms

To add new sales data, we will use an Angular Material 9 Reactive Forms. After data POST to REST API successfully then it emits a message to the socket.io. Open and edit `src/app/add-sales/add-sales.component.ts` then add these imports.

import * as io from 'socket.io-client';
import { Router } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';

Create a new class before the main class `@Components` that implementing Angular ErrorStateMatcher.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Inject the imported modules to the constructor of the main class.

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

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

  socket = io('http://localhost:4000');

  salesForm: FormGroup;
  itemId = '';
  itemName = '';
  itemPrice: number = null;
  itemQty: number = null;
  totalPrice: number = null;
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

Add initial validation for each field.

  ngOnInit(): void {
    this.salesForm = this.formBuilder.group({
      itemId : [null, Validators.required],
      itemName : [null, Validators.required],
      itemPrice : [null, Validators.required],
      itemQty : [null, Validators.required],
      totalPrice : [null, Validators.required]
    });
  }

Create a function for submitting or POST sales form and emit a message to socket.io after successfully submit data.

  onFormSubmit() {
    this.isLoadingResults = true;
    this.api.addSales(this.salesForm.value)
      .subscribe((res: any) => {
          const id = res._id;
          this.isLoadingResults = false;
          this.socket.emit('updatedata', res);
          this.router.navigate(['/sales-details', id]);
        }, (err: any) => {
          console.log(err);
          this.isLoadingResults = false;
        });
  }

Next, open and edit `src/app/add-sales/add-sales` then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <h2>Add Sales</h2>
  <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]="['/']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="salesForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <mat-label>Item ID</mat-label>
        <input matInput placeholder="Item ID" formControlName="itemId"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemId').valid && salesForm.get('itemId').touched">Please enter Item ID</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Item Name</mat-label>
        <input matInput placeholder="Item Name" formControlName="itemName"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemName').valid && salesForm.get('itemName').touched">Please enter Item Name</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Item Price</mat-label>
        <input matInput type="number" placeholder="Item Price" formControlName="itemPrice"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemPrice').valid && salesForm.get('itemPrice').touched">Please enter Item Price</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Item Qty</mat-label>
        <input matInput type="number" placeholder="Item Qty" formControlName="itemQty"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemQty').valid && salesForm.get('itemQty').touched">Please enter Item Qty</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Total Price</mat-label>
        <input matInput type="number" placeholder="Total Price" formControlName="totalPrice"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('totalPrice').valid && salesForm.get('totalPrice').touched">Please enter Total Price</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!salesForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit `src/app/add-sales/add-sales.component.scss` then add this 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(0) {
  margin-bottom: 10px;
}

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

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


Step #11: Edit Data using Angular Material Reactive Forms

We already put an edit button inside the Sales Details component for the call Edit page. Now, open and edit `src/app/edit-sales/edit-sales.component.ts` then add these imports.

import * as io from 'socket.io-client';
import { Router, ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';

Create a new class before the main class `@Components`.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Inject the imported modules to the constructor of the main class.

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

Declare the socket.io and Form Group variable and all of the required variables for the sales-form before the constructor.

  socket = io('http://localhost:4000');

  salesForm: FormGroup;
  _id = '';
  itemId = '';
  itemName = '';
  itemPrice: number = null;
  itemQty: number = null;
  totalPrice: number = null;
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

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

  ngOnInit(): void {
    this.getSalesById(this.route.snapshot.params.id);
    this.salesForm = this.formBuilder.group({
      itemId : [null, Validators.required],
      itemName : [null, Validators.required],
      itemPrice : [null, Validators.required],
      itemQty : [null, Validators.required],
      totalPrice : [null, Validators.required]
    });
  }

Create a function for getting sales data by the ID that will fill to each form field.

  getSalesById(id: any) {
    this.api.getSalesById(id).subscribe((data: any) => {
      this._id = data._id;
      this.salesForm.setValue({
        itemId: data.itemId,
        itemName: data.itemName,
        itemPrice: data.itemPrice,
        itemQty: data.itemQty,
        totalPrice: data.totalPrice
      });
    });
  }

Create a function to update the sales changes then emit the change message to the socket.io.

  onFormSubmit() {
    this.isLoadingResults = true;
    this.api.updateSales(this._id, this.salesForm.value)
      .subscribe((res: any) => {
          const id = res._id;
          this.isLoadingResults = false;
          this.socket.emit('updatedata', res);
          this.router.navigate(['/sales-details', id]);
        }, (err: any) => {
          console.log(err);
          this.isLoadingResults = false;
        }
      );
  }

Add a function for handling the show sales details button.

  salesDetails() {
    this.router.navigate(['/sales-details', this._id]);
  }

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

<div class="example-container mat-elevation-z8">
  <h2>Edit Sales</h2>
  <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]="['/']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="salesForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <mat-label>Item ID</mat-label>
        <input matInput placeholder="Item ID" formControlName="itemId"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemId').valid && salesForm.get('itemId').touched">Please enter Item ID</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Item Name</mat-label>
        <input matInput placeholder="Item Name" formControlName="itemName"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemName').valid && salesForm.get('itemName').touched">Please enter Item Name</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Item Price</mat-label>
        <input matInput type="number" placeholder="Item Price" formControlName="itemPrice"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemPrice').valid && salesForm.get('itemPrice').touched">Please enter Item Price</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Item Qty</mat-label>
        <input matInput type="number" placeholder="Item Qty" formControlName="itemQty"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('itemQty').valid && salesForm.get('itemQty').touched">Please enter Item Qty</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Total Price</mat-label>
        <input matInput type="number" placeholder="Total Price" formControlName="totalPrice"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!salesForm.get('totalPrice').valid && salesForm.get('totalPrice').touched">Please enter Total Price</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!salesForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit `src/app/edit-sales/edit-sales.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(0) {
  margin-bottom: 10px;
}

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

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


Step #12: Run and Test a Complete MEAN Stack Angular 9 App

Now, it's a time to run the MEAN stack Angular 9 app together. First, run the MongoDB server in another terminal tab.

mongod

Open a new terminal tab then open the mongo command.

mongo

Go to the sales-report database then import these initial data.

use sales-report
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-01')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-02')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 2, totalPrice: 1996, updated: new Date('2020-02-03')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-04')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 3, totalPrice: 2994, updated: new Date('2020-02-05')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-06')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-07')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-08')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-09')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-10')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 2, totalPrice: 2198, updated: new Date('2020-02-11')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-12')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-13')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-14')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 3, totalPrice: 3297, updated: new Date('2020-02-15')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-16')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-17')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-18')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 2, totalPrice: 998, updated: new Date('2020-02-01')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 3, totalPrice: 1497, updated: new Date('2020-02-02')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 1, totalPrice: 499, updated: new Date('2020-02-03')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 1, totalPrice: 499, updated: new Date('2020-02-04')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 1, totalPrice: 499, updated: new Date('2020-02-05')});
db.sales.insert({itemId: '10004', itemName: 'Asus ROG 2 Phone', itemPrice: 1199, itemQty: 1, totalPrice: 1199, updated: new Date('2020-02-06')});
db.sales.insert({itemId: '10004', itemName: 'Asus ROG 2 Phone', itemPrice: 1199, itemQty: 1, totalPrice: 1199, updated: new Date('2020-02-07')});
db.sales.insert({itemId: '10004', itemName: 'Asus ROG 2 Phone', itemPrice: 1199, itemQty: 1, totalPrice: 1199, updated: new Date('2020-02-08')});
db.sales.insert({itemId: '10005', itemName: 'Samsung Galaxy Fold', itemPrice: 1299, itemQty: 1, totalPrice: 1299, updated: new Date('2020-02-09')});
db.sales.insert({itemId: '10005', itemName: 'Samsung Galaxy Fold', itemPrice: 1299, itemQty: 1, totalPrice: 1299, updated: new Date('2020-02-10')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 2, totalPrice: 2798, updated: new Date('2020-02-11')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-12')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 3, totalPrice: 4197, updated: new Date('2020-02-13')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-14')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-15')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-16')});
db.sales.insert({itemId: '10007', itemName: 'iPhone 11 Pro', itemPrice: 1499, itemQty: 2, totalPrice: 2998, updated: new Date('2020-02-17')});
db.sales.insert({itemId: '10007', itemName: 'iPhone 11 Pro', itemPrice: 1499, itemQty: 1, totalPrice: 1499, updated: new Date('2020-02-18')});
db.sales.insert({itemId: '10008', itemName: 'iPhone 11', itemPrice: 899, itemQty: 3, totalPrice: 2697, updated: new Date('2020-02-01')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-02')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-03')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-04')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-05')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-06')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-07')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-08')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-09')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-10')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-11')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-12')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-13')});

Next, run the Node-Express REST API server at the root of this project.

nodemon

Then run the Angular 9 app on the other terminal tab inside the client folder.

cd client
ng serve --open

And here they are the full MEAN stack with Angular 9 in the browser.

MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly - List and Chart
MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly - Details
MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly - edit
MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly - Add

That it's, the MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly. you can get the full source code on 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…