Angular 8 Universal and MongoDB Server-side Rendering (SSR)

by Didin J. on Jul 08, 2019 Angular 8 Universal and MongoDB Server-side Rendering (SSR)

A comprehensive step by step tutorial to building a full CRUD Angular 8 Universal and MongoDB Server-side Rendering (SSR)

A comprehensive step by step tutorial to building a full CRUD Angular 8 Universal and MongoDB Server-side Rendering (SSR). For more detailed information about Angular 8 Universal and SSR, you can find in the official Angular blog. Here. we will practically use Angular 8 universal for a CRUD (Create, Read, Update, Delete) operation application that store the data to the MongoDB server.

List of the steps:

The Angular 8 application that we will build is a very simple application about Article CRUD. This might be a starting point to build your own complete blog engine or news engine because this application should be matched for SEO requirements.

Almost all codes in this application using Typescript and ES6 except a few files that still use plain ES5 code that uses by Express.js. For that, we need your suggestion for converting to working Angular 8/Typescript code styles. The following tools, frameworks, and modules are required for this tutorial:

  1. Node.js (Recommended version)
  2. Angular 8
  3. Angular Universal (SSR)
  4. MongoDB
  5. Mongoose.js
  6. Body-parser
  7. Postman
  8. Terminal or Command Line
  9. IDE or Text Editor

As usual, before we move forward to the main steps of Angular 8 Universal and MongoDB Server-side Rendering (SSR). We have to check the latest Node and Angular versions then update them if necessary. Just type this command to see it.

ng version

You will see the currently installed Node, Angular and Angular CLI versions.

Angular CLI: 8.0.6
Node: 10.15.1
OS: darwin x64
Angular: 8.0.3


Create a New Angular 8 Application and Add Angular Universal

First, we will create a new Angular 8 application by type this command in the terminal.

ng new article-crud

Next, go to the newly created Angular 8 application folder then type this command to add Angular Universal SSR.

ng add @nguniversal/express-engine --clientProject article-crud

That command just added and updated a few files.

CREATE src/main.server.ts (361 bytes)
CREATE src/app/app.server.module.ts (427 bytes)
CREATE tsconfig.server.json (204 bytes)
CREATE webpack.server.config.js (1466 bytes)
CREATE server.ts (1980 bytes)
UPDATE package.json (1844 bytes)
UPDATE angular.json (4263 bytes)
UPDATE src/main.ts (432 bytes)
UPDATE src/app/app.module.ts (438 bytes)

Next, check the Angular Universal SSR application by running this application using these commands.

npm run build:ssr && npm run serve:ssr

Now, after go to `localhost:4000` you see this standard Angular application page and feel the difference with the page that rendered in the browser.

Angular 8 Universal and MongoDB Server-side Rendering (SSR) - Welcome


Install and Configure Mongoose.js and Required Modules

To store and retrieve data from the MongoDB server, we will use Mongoose.js. To install it, just run these commands that including `body-parser` modules.

npm install --save mongoose body-parser

In this Angular 8 Universal SSR, body-parser will use to parse the request body to the API. Next, open and edit `server.ts` in the root project folder then add these imports of Mongoose and body-parser.

import * as mongoose from 'mongoose';
import bodyParser from "body-parser";

Next, add these lines of codes after the imports lines to connect to the MongoDB and use body-parser for every JSON requests.

mongoose.connect('mongodb://localhost/angular8-crud', { useNewUrlParser: true, useFindAndModify: false })
  .then(() =>  console.log('connection successful'))
  .catch((err) => console.error(err));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));


Create a Mongoose Schema and Router

We will create a Mongoose schema or model that represent MongoDB fields. Create a models folder and a file to holds all required Article fields.

mkdir models
touch models/article.ts

Next, fill that file with this Mongoose Schema.

import mongoose, { Schema } from 'mongoose';

const ArticleSchema: Schema = new Schema({
  title: { type: String, required: true },
  author: { type: String, required: true },
  description: { type: String, required: true },
  content: { type: String, required: true },
  updatedAt: { type: Date, default: Date.now }
});

export default mongoose.model('Article', ArticleSchema);

Next, we will create a route that accesses the MongoDB data via API. Create a new folder call `routes` and a file inside it.

mkdir routes
touch routes/article-route.ts

Open and edit that file then add these imports of Express Request, Response, NextFunction, and Article schema.

import { Request, Response, NextFunction } from 'express';
import Article from '../models/article';

Add these lines of codes to create a CRUD method of REST API. The route path starts with `/api/` that will be called from the Angular 8 application using full URL `http://localhost:4000/api/`.

export class ArticleRoute {

  public articleRoute(app): void {
    app.route('/api/').get((req: Request, res: Response, next: NextFunction) => {
      Article.find((err, articles) => {
        if (err) { return next(err); }
        res.json(articles);
      });
    });

    app.route('/api/:id').get((req: Request, res: Response, next: NextFunction) => {
      Article.findById(req.params.id, (err, article) => {
        if (err) { return next(err); }
        res.json(article);
      });
    });

    app.route('/api/').post((req: Request, res: Response, next: NextFunction) => {
      console.log(req.body);
      Article.create(req.body, (err, article) => {
        if (err) { return next(err); }
        res.json(article);
      });
    });

    app.route('/api/:id').put((req: Request, res: Response, next: NextFunction) => {
      Article.findByIdAndUpdate(req.params.id, req.body, (err, article) => {
        if (err) { return next(err); }
        res.json(article);
      });
    });

    app.route('/api/:id').delete((req: Request, res: Response, next: NextFunction) => {
      Article.findByIdAndRemove(req.params.id, req.body, (err, article) => {
        if (err) { return next(err); }
        res.json(article);
      });
    });
  }
}

Next, open and edit `server.ts` then add this import.

import { ArticleRoute } from './routes/article-route';

Declare a constant after the import lines.

const articleRoute: ArticleRoute = new ArticleRoute();

Add this line before the `all regular route` lines.

articleRoute.articleRoute(app);

Don't declare API route after all regular route otherwise the API URL won't working.


Test the Angular 8 Universal SSR REST API

We will test the Angular 8 Universal SSR REST API using the Postman app. First, run the app using this command.

npm run build:ssr && npm run serve:ssr

Open the Postman app then change the method to GET and address to `localhost:4000/api/` then press `SEND` button. If there's any data in your MongoDB server then it's will be similar like this.

Angular 8 Universal and MongoDB Server-side Rendering (SSR) - Postman

To save data, change the method to POST with the same address then choose RAW body with type JSON. Fill the RAW body with this example or your own data example.

{
    "title": "Odong",
    "author": "Odong",
    "description": "Odong",
    "content": "Odong"
}

You will see this result for successful POST data.

Angular 8 Universal and MongoDB Server-side Rendering (SSR) - Postman POST

Next, you can test the rest of PUT and DELETE using your own data to make sure the Angular 8 Universal SSR API working.


Create Angular 8 Routes for Page Navigation

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

ng g component articles --skip-import
ng g component show-article --skip-import
ng g component add-article --skip-import
ng g component edit-article --skip-import

There's an additional `--skip-import` variable because there will be a conflict between 2 modules, Angular 8 and Server modules.

More than one module matches. Use skip-import option to skip importing the component into the closest module.

Next, we will manually add or register those components to the `src/app/app.module.ts`. Add these imports of the created Angular components to that file.

import { ArticlesComponent } from './articles/articles.component';
import { ShowArticleComponent } from './show-article/show-article.component';
import { AddArticleComponent } from './add-article/add-article.component';
import { EditArticleComponent } from './edit-article/edit-article.component';

Then add those components to `@NgModule` declaration.

@NgModule({
  declarations: [
    ...
    ArticlesComponent,
    ShowArticleComponent,
    AddArticleComponent,
    EditArticleComponent
  ],
  ...
})

Next, open and edit `src/app/app-routing.module.ts` then add these imports.

const routes: Routes = [
  {
    path: 'articles',
    component: ArticlesComponent,
    data: { title: 'Articles' }
  },
  {
    path: 'show-article/:id',
    component: ShowArticleComponent,
    data: { title: 'Show Article' }
  },
  {
    path: 'add-article',
    component: AddArticleComponent,
    data: { title: 'Add Article' }
  },
  {
    path: 'edit-article/:id',
    component: EditArticleComponent,
    data: { title: 'Edit Article' }
  },
  { path: '',
    redirectTo: '/articles',
    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 that will be wrapped inside <router-outlet>.

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


Create an Angular 8 Service using HttpClient, Observable and RXJS

To access REST API from Angular 8, we need to create an Angular 8 service which will handle all POST, GET, UPDATE, DELETE requests. The response from the REST API emitted by Observable that can subscribe and read from the Components. For error handler and data Extraction, we will use RXJS Library. 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 this import.

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

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

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

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

export class Article {
  _id: string;
  title: string;
  author: string;
  description: string;
  content: string;
  updatedAt: Date;
}

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

ng g service api

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

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

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<T>(operation = 'operation', result?: T) {
  return (error: any): Observable<T> => {
    console.error(error); // log to console instead
    return of(result as T);
  };
}

Add the functions for all CRUD (create, read, update, delete) RESTful call of article data.

getArticles(): Observable<Article[]> {
  return this.http.get<Article[]>(apiUrl)
    .pipe(
      tap(articles => console.log('fetched Articles')),
      catchError(this.handleError('getArticles', []))
    );
}

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

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

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

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

You can find more details about Angular 8 Observable and RXJS here.


Display List of Articles using Angular 8 Material

We will display the list of articles published from API Service. The data published from the API service read by subscribing as an Article model in the Angular 8 component. For that, open and edit `src/app/articles/articles.component.ts` then add these imports.

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

Next, inject the API Service to the constructor.

constructor(private api: ApiService) { }

Next, for the user interface (UI) we will use Angular 8 Material and CDK. There's a CLI 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 8 Material.

ng add @angular/material

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

? Choose a prebuilt theme name, or "custom" for a custom theme: Purple/Green       [ Preview: h
ttps://material.angular.io?theme=purple-green ]
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

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

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: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  AppRoutingModule,
  ReactiveFormsModule,
  BrowserAnimationsModule,
  MatInputModule,
  MatTableModule,
  MatPaginatorModule,
  MatSortModule,
  MatProgressSpinnerModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, back to `src/app/articles/articles.component.ts` then add these imports.

import { Article } from '../article';

Declare the variables of Angular 8 Material Table Data Source before the constructor.

displayedColumns: string[] = ['title', 'author'];
data: Article[] = [];
isLoadingResults = true;

Modify the `ngOnInit` function to get a list of articles immediately.

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

Next, open and edit `src/app/articles/articles.component.html` then replace all HTML tags with this Angular Material 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]="['/add-article']"><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">

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

      <!-- Article Author 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]="['/show-article/', row._id]"></tr>
    </table>
  </div>
</div>

Finally, to make a little UI adjustment, open and edit `src/app/articles/articles.component.scss` then add this CSS 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;
}


Show and Delete an Article Details using Angular 8 Material

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

import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Article } from '../article';

Inject above modules to the constructor.

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

Declare the variables before the constructor for hold article data that get from the API.

article: Article = { _id: '', title: '', author: '', description: '', content: '', updatedAt: null };
isLoadingResults = true;

Add a function for getting Article data from the API.

getArticleDetails(id: any) {
  this.api.getArticle(id)
    .subscribe((data: any) => {
      this.article = data;
      console.log(this.article);
      this.isLoadingResults = false;
    });
}

Call that function when the component is initiated.

ngOnInit() {
  this.getArticleDetails(this.route.snapshot.params.id);
}

Add this function for delete article.

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

For the view, open and edit `src/app/article-detail/article-detail.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]="['/articles']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <mat-card-header>
      <mat-card-title><h2>{{article.title}}</h2></mat-card-title>
      <mat-card-subtitle>{{article.author}} | {{article.updatedAt}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <h3>{{article.description}}</h3>
      <p>{{article.content}}</p>
    </mat-card-content>
    <mat-card-actions>
      <a mat-flat-button color="primary" [routerLink]="['/edit-article', article._id]"><mat-icon>edit</mat-icon></a>
      <a mat-flat-button color="warn" (click)="deleteArticle(article._id)"><mat-icon>delete</mat-icon></a>
    </mat-card-actions>
  </mat-card>
</div>

Finally, open and edit `src/app/article-detail/article-detail.component.scss` then add this lines of CSS 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;
}


Add an Article using Angular 8 Material

To create a form for adding an Article, open and edit `src/app/add-article/add-article.component.ts` then add these imports.

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.

articleForm: FormGroup;
title = '';
author = '';
description = '';
content = '';
isLoadingResults = false;

Add initial validation for each field.

ngOnInit() {
  this.articleForm = this.formBuilder.group({
    'title' : [null, Validators.required],
    'author' : [null, Validators.required],
    'description' : [null, Validators.required],
    'content' : [null, Validators.required]
  });
}

Create a function for submitting or POST article form.

onFormSubmit() {
  this.isLoadingResults = true;
  this.api.addArticle(this.articleForm.value)
    .subscribe((res: any) => {
        const id = res._id;
        this.isLoadingResults = false;
        this.router.navigate(['/show-article', id]);
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      });
}

Next, add this import for implementing `ErrorStateMatcher`.

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

Instantiate that `MyErrorStateMatcher` as a variable in the main class.

matcher = new MyErrorStateMatcher();

Next, open and edit `src/app/add-article/add-article.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]="['/articles']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="articleForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('title').valid && articleForm.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="!articleForm.get('author').valid && articleForm.get('author').touched">Please enter Author</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('description').valid && articleForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <textarea matInput placeholder="Content" formControlName="content"
               [errorStateMatcher]="matcher"></textarea>
        <mat-error>
          <span *ngIf="!articleForm.get('content').valid && articleForm.get('content').touched">Please enter Content</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!articleForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

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


Edit an Article using Angular 8 Material

We have put an edit button inside the Article Detail component for call Edit page. Now, open and edit `src/app/edit-article/edit-article.component.ts` then add these imports.

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 article form before the constructor.

articleForm: FormGroup;
_id = '';
title = '';
author = '';
description = '';
content = '';
isLoadingResults = false;

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

ngOnInit() {
  this.getArticle(this.route.snapshot.params.id);
  this.articleForm = this.formBuilder.group({
    'title' : [null, Validators.required],
    'author' : [null, Validators.required],
    'description' : [null, Validators.required],
    'content' : [null, Validators.required]
  });
}

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

getArticle(id: any) {
  this.api.getArticle(id).subscribe((data: any) => {
    this._id = data._id;
    this.articleForm.setValue({
      title: data.title,
      author: data.author,
      description: data.description,
      content: data.content
    });
  });
}

Create a function to update the article changes.

onFormSubmit() {
  this.isLoadingResults = true;
  this.api.updateArticle(this._id, this.articleForm.value)
    .subscribe((res: any) => {
        const id = res._id;
        this.isLoadingResults = false;
        this.router.navigate(['/show-article', id]);
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      }
    );
}

Add a function for handling the show article details button.

articleDetails() {
  this.router.navigate(['/show-article', this._id]);
}

Next, add this import for implementing `ErrorStateMatcher`.

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

Instantiate that `MyErrorStateMatcher` as a variable in the main class.

matcher = new MyErrorStateMatcher();

Next, open and edit `src/app/edit-article/edit-article.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)="articleDetails()"><mat-icon>info</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="articleForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('title').valid && articleForm.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="!articleForm.get('author').valid && articleForm.get('author').touched">Please enter Author</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('description').valid && articleForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <textarea matInput placeholder="Content" formControlName="content"
               [errorStateMatcher]="matcher"></textarea>
        <mat-error>
          <span *ngIf="!articleForm.get('content').valid && articleForm.get('content').touched">Please enter Content</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!articleForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

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


Test and Run Angular 8 Universal and MongoDB Server-side Rendering (SSR)

It's time to test and try the performance of Angular 8 Universal and MongoDB Server-side Rendering (SSR). First, run the MongoDB server in the different terminal or command line.

mongod

Next, run the Angular 8 Universal SSR in the current terminal and project folder.

npm run build:ssr && npm run serve:ssr

After build finished for both client and server then open the application in the browser `http://localhost:4200/` and you will see and feel this Angular 8 application as below.

Angular 8 Universal and MongoDB Server-side Rendering (SSR) - List page
Angular 8 Universal and MongoDB Server-side Rendering (SSR) - Add/Edit Page
Angular 8 Universal and MongoDB Server-side Rendering (SSR) - Details Page

That it's, the example of Angular 8 Universal and MongoDB Server-side Rendering (SSR). You can get the full source codes 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!