In this Angular 10 tutorial, we will show you a practical exercise on building a CRUD web application using Angular Universal Server-side rendering (SSR). Server-side rendering means a client-side (Angular app) render on the server instead of the browser to get less load to the client-side. The server-side using Express-engine and MongoDB for the datastore.
This tutorial divided into several steps:
- Step #1: Create an Angular Universal Application
- Step #2: Add Mongoose.js Schema and Router
- Step #3: Add Angular Routes and Navigation
- Step #4: Add Angular Service
- Step #5: Add Angular Material UI/UX for CRUD operation
- Step #6: Deploy, Run, and Test the Angular Universal SSR on VPS
The Angular Universal application that we will build is a very simple application of create, read, update, delete (CRUD) operation. At the end, we will show you how to deploy it to the VPS.
The following tools, frameworks, libraries, and modules are required for this tutorial:
- Node.js
- Angular Universal with Express-Engine
- MongoDB and Mongoose.js
- Angular Material
- VPS contains Ubuntu, Nginx, PM2, and MongoDB
- Terminal or CMD
- IDE or Text Editor (we are using VSCode)
Let's get started with the main steps!
You can watch the video tutorial on our YouTube channel here.
Step #1: Create an Angular Universal Application
As usual, we will start this tutorial by preparing the environment to create or build an Angular application inside the Node.js ecosystem. First, make sure you have download the latest and recommended Node.js version and can run NPM in the Terminal or CMD. To check the existing Node and NPM, type this command.
node -v
v12.18.0
npm -v
6.14.5
Install or update the latest Angular CLI by type this command.
sudo npm install -g @angular/cli
Next, create a new Angular application by type this command.
ng new angular-ssr
If there are the questions, give them the answers like this.
? 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 10 application folder then type this command to add Angular Universal SSR.
cd ./angular-ssr
ng add @nguniversal/express-engine --clientProject angular-ssr
Next, check the Angular Universal SSR application by running this application using these commands.
npm run build:ssr && npm run serve:ssr
Open the browser then go to 'localhost:4000' and you will this standard Angular welcome page.
Step #2: Add Mongoose.js Schema and Router
We will use Mongoose.js as ODM for MongoDB. The MongoDB collection will be represented by the Mongoose.js model. Add the Mongoose.js and required dependencies to this Angular Universal project by type this command.
npm install --save mongoose @types/mongoose
Next, open this Angular Universal project using your IDE or text editor. To use Visual Studio Code, just type this command.
code .
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';
Next, add these lines of codes inside the export function body after the constant variables to connect to MongoDB.
mongoose.connect('mongodb://localhost/angular-ssr', { useNewUrlParser: true, useFindAndModify: false, useUnifiedTopology: true })
.then(() => console.log('connection successful'))
.catch((err) => console.error(err));
Add these lines after the server.set code to parse the JSON body on the request.
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
Next, add a new folder for Mongoose models and add a mongoose model inside that folder.
mkdir models
touch models/book.ts
Fill that file with these codes of Mongoose Schema.
import mongoose, { Schema } from 'mongoose';
const BookSchema: Schema = new Schema({
isbn: { type: String, required: true },
title: { type: String, required: true },
author: { type: String, required: true },
description: { type: String, required: true },
publisher: { type: String, required: true },
publishYear: { type: String, required: true },
updatedAt: { type: Date, default: Date.now }
});
export default mongoose.model('Book', BookSchema);
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/book-route.ts
Open and edit that file then add these imports of Express Request, Response, NextFunction, and Book schema.
import { Request, Response, NextFunction } from 'express';
import Book from '../models/book';
Add these lines of codes to create a CRUD method of REST API. The route path starts with `/book/` that will be called from the Angular 10 application using full URL `http://localhost:4000/book/`.
export class BookRoute {
public bookRoute(app: any): void {
app.route('/book/').get((req: Request, res: Response, next: NextFunction) => {
Book.find((err, books) => {
if (err) { return next(err); }
res.json(books);
});
});
app.route('/book/:id').get((req: Request, res: Response, next: NextFunction) => {
Book.findById(req.params.id, (err, book) => {
if (err) { return next(err); }
res.json(book);
});
});
app.route('/book/').post((req: Request, res: Response, next: NextFunction) => {
console.log(req.body);
Book.create(req.body, (err, book) => {
if (err) { return next(err); }
res.json(book);
});
});
app.route('/book/:id').put((req: Request, res: Response, next: NextFunction) => {
Book.findByIdAndUpdate(req.params.id, req.body, (err, book) => {
if (err) { return next(err); }
res.json(book);
});
});
app.route('/book/:id').delete((req: Request, res: Response, next: NextFunction) => {
Book.findByIdAndRemove(req.params.id, req.body, (err, book) => {
if (err) { return next(err); }
res.json(book);
});
});
}
}
Next, open and edit `server.ts` then add this import.
import { BookRoute } from './routes/book-route';
Declare a constant after other constants.
const bookRoute: BookRoute = new BookRoute();
Add this line before the server.get.
bookRoute.bookRoute(server);
Don't declare API route after all regular route otherwise the API URL won't working.
Step #3: Add Angular Routes and Navigation
To create Angular 10 Routes for navigation between Angular 10 pages/component, add or generate all required components.
ng g component books --skip-import
ng g component book-details --skip-import
ng g component book-add --skip-import
ng g component book-edit --skip-import
There's an additional `--skip-import` variable because there will be a conflict between 2 modules, Angular 10 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 { BooksComponent } from './books/books.component';
import { BookDetailsComponent } from './book-details/book-details.component';
import { BookAddComponent } from './book-add/book-add.component';
import { BookEditComponent } from './book-edit/book-edit.component';
Then add those components to the `@NgModule` declaration.
@NgModule({
declarations: [
...
BooksComponent,
BookDetailsComponent,
BookAddComponent,
BookEditComponent
],
...
})
Next, open and edit `src/app/app-routing.module.ts` then add these imports.
import { BooksComponent } from './books/books.component';
import { BookDetailsComponent } from './book-details/book-details.component';
import { BookAddComponent } from './book-add/book-add.component';
import { BookEditComponent } from './book-edit/book-edit.component';
Modify the routes' constant variable to this.
const routes: Routes = [
{
path: 'books',
component: BooksComponent,
data: { title: 'Books' }
},
{
path: 'book-details/:id',
component: BookDetailsComponent,
data: { title: 'Book Details' }
},
{
path: 'book-add',
component: BookAddComponent,
data: { title: 'Add Book' }
},
{
path: 'book-edit/:id',
component: BookEditComponent,
data: { title: 'Edit Book' }
},
{ path: '',
redirectTo: '/books',
pathMatch: 'full'
}
];
Next, 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;
}
Step #4: Add Angular Service
To access REST API from Angular 10 Universal, we need to create an Angular 10 service that 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 { HttpClientModule } from '@angular/common/http';
Add it to `@NgModule` imports after `BrowserModule`.
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
AppRoutingModule,
HttpClientModule
],
We will use the type specifier to get a typed result object. For that, create a new Typescript file `src/app/book.ts` then add these lines of Typescript codes.
export class Book {
_id: string;
isbn: string;
title: string;
author: string;
description: string;
publisher: string;
publishedYear: string;
updatedAt: Date;
}
Next, generate an Angular 10 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, throwError, catchError, Angular HttpClient, HttpHeaders, HttpErrorResponse, and Article object model.
import { Observable, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { Book } from './book';
Add this constant of full URL before the `@Injectable`.
const apiUrl = 'http://192.168.0.7:4000/book';
Inject the `HttpClient` module to the constructor.
constructor(private http: HttpClient) {}
Add the error handler function.
private handleError(error: HttpErrorResponse): any {
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.message}`);
return throwError(
'Something bad happened; please try again later.');
}
Add the functions for all CRUD (create, read, update, delete) operation call to REST API.
getBooks(): Observable<any> {
return this.http.get<Book[]>(apiUrl)
.pipe(
catchError(this.handleError)
);
}
getBook(id: number): Observable<any> {
const url = `${apiUrl}/${id}`;
return this.http.get<Book>(url).pipe(
catchError(this.handleError)
);
}
addBook(book: Book): Observable<any> {
return this.http.post<Book>(apiUrl, book).pipe(
catchError(this.handleError)
);
}
updateBook(id: any, book: Book): Observable<any> {
const url = `${apiUrl}/${id}`;
return this.http.put(url, book).pipe(
catchError(this.handleError)
);
}
deleteBook(id: any): Observable<any> {
const url = `${apiUrl}/${id}`;
return this.http.delete<Book>(url).pipe(
catchError(this.handleError)
);
}
You can find more details about Angular 8 Observable and RXJS here.
Step #5: Add Angular Material UI/UX for CRUD operation
Before implementing UI/UX for CRUD operation using Angular Material, first, install all required Angular modules and schematics.
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: Indigo/Pink
[ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? No
? Set up browser animations for Angular Material? Yes
We will register all required Angular 10 Material components or modules to `src/app/app.module.ts`. Open and edit that file then add these required imports of Angular Material components, forms, and reactive form module.
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
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 those modules to `@NgModule` imports.
imports: [
...
FormsModule,
ReactiveFormsModule,
MatInputModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatSortModule,
MatTableModule,
MatIconModule,
MatButtonModule,
MatCardModule,
MatFormFieldModule
],
Next, open and edit `src/app/books/books.component.ts` then add these imports.
import { ApiService } from '../api.service';
import { Book } from '../book';
Inject the ApiService to the constructor.
constructor(private api: ApiService) { }
Declare the variables of Angular 10 Material Table Data Source before the constructor.
displayedColumns: string[] = ['title', 'author'];
data: Book[] = [];
isLoadingResults = true;
Modify the `ngOnInit` function to get a list of articles immediately.
ngOnInit(): void {
this.api.getBooks()
.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/books/books.component.html` then replace all HTML tags with this Angular Material component that displays the Angular Table.
<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]="['/book-add']"><mat-icon>add</mat-icon></a>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="data" class="example-table"
matSort matSortActive="title" matSortDisableClear matSortDirection="asc">
<!-- 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]="['/book-details/', row._id]"></tr>
</table>
</div>
</div>
Finally, to make a little UI adjustment, open and edit `src/app/books/books.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;
}
Next, to show book details after click or tap on the one of a row inside the Angular 10 Material table, open and edit `src/app/book-details/book-details.component.ts` then add these imports.
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Book } from '../book';
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 book data that get from the API.
book: Book = {
_id: '',
isbn: '',
title: '',
author: '',
description: '',
publisher: '',
publishedYear: '',
updatedAt: null
};
isLoadingResults = true;
Add a function for getting Book data from the API.
getBookDetails(id: any): void {
this.api.getBook(id)
.subscribe((data: any) => {
this.book = data;
console.log(this.book);
this.isLoadingResults = false;
});
}
Call that function when the component is initiated.
ngOnInit(): void {
this.getBookDetails(this.route.snapshot.paramMap.get('id'));
}
Add this function to delete a book.
deleteBook(id: any): void {
this.isLoadingResults = true;
this.api.deleteBook(id)
.subscribe(() => {
this.isLoadingResults = false;
this.router.navigate(['/books']);
}, (err) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
For the view, open and edit `src/app/book-detaails/book-detaails.component.html` then replace all HTML tags with this.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/books']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<mat-card-header>
<mat-card-title><h2>{{book.title}}</h2></mat-card-title>
<mat-card-subtitle>{{book.author}} | {{book.updatedAt}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<h3>ISBN: {{book.isbn}}</h3>
<h3>Description: {{book.description}}</h3>
<h3>Publisher: {{book.publisher}}</h3>
<h3>Published Year:{{book.publishedYear}}</h3>
</mat-card-content>
<mat-card-actions>
<a mat-flat-button color="primary" [routerLink]="['/book-edit', book._id]"><mat-icon>edit</mat-icon></a>
<a mat-flat-button color="warn" (click)="deleteBook(book._id)"><mat-icon>delete</mat-icon></a>
</mat-card-actions>
</mat-card>
</div>
Finally, open and edit `src/app/book-details/book-details.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;
}
Next, to create a form for adding a book, open and edit `src/app/book-add/book-add.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';
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 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, loading spinner trigger, and initialize ErrorStateMatcher inside the form before the constructor.
bookForm: FormGroup;
isbn = '';
book = '';
author = '';
description = '';
publisher = '';
publishedYear = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
Add initial validation for each field.
ngOnInit(): void {
this.bookForm = this.formBuilder.group({
'isbn' : [null, Validators.required],
'title' : [null, Validators.required],
'author' : [null, Validators.required],
'description' : [null, Validators.required],
'publisher' : [null, Validators.required],
'publishedYear' : [null, Validators.required]
});
}
Create a function for submitting or POST book form.
onFormSubmit(): void {
this.isLoadingResults = true;
this.api.addBook(this.bookForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.router.navigate(['/book-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
});
}
Next, open and edit `src/app/book-add/book-add.component.html` then replace all HTML tags with this.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/books']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="bookForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<input matInput placeholder="ISBN" formControlName="isbn"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('isbn').valid && bookForm.get('isbn').touched">Please enter ISBN</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Title" formControlName="title"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('title').valid && bookForm.get('title').touched">Please enter Title</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Author" formControlName="author"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('author').valid && bookForm.get('author').touched">Please enter Author</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Description" formControlName="description"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('description').valid && bookForm.get('description').touched">Please enter Description</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Publisher" formControlName="publisher"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('publisher').valid && bookForm.get('publisher').touched">Please enter Publisher</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Published Year" formControlName="publishedYear"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('publishedYear').valid && bookForm.get('publishedYear').touched">Please enter Published Year</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!bookForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Finally, open and edit `src/app/book-add/book-add.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;
}
Next, open and edit `src/app/book-edit/book-edit.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';
import { ErrorStateMatcher } from '@angular/material/core';
Create a new class before the main class `@Components` to catch the error in the form entry.
/** 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 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 of a book form, loading spinner trigger, and ErrorStateMatcher before the constructor.
bookForm: FormGroup;
_id = '';
isbn = '';
title = '';
author = '';
description = '';
publisher = '';
publishedYear = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
Next, add validation for all fields when the component is initiated.
ngOnInit(): void {
this.getBook(this.route.snapshot.paramMap.get('id'));
this.bookForm = this.formBuilder.group({
'isbn' : [null, Validators.required],
'title' : [null, Validators.required],
'author' : [null, Validators.required],
'description' : [null, Validators.required],
'publisher' : [null, Validators.required],
'publishedYear' : [null, Validators.required]
});
}
Create a function for getting book data that filled to each form field.
getBook(id: any): void {
this.api.getBook(id).subscribe((data: any) => {
this._id = data._id;
this.bookForm.setValue({
isbn: data.isbn,
title: data.title,
author: data.author,
description: data.description,
publisher: data.publisher,
publishedYear: data.publishedYear
});
});
}
Create a function to update the book changes.
onFormSubmit(): void {
this.isLoadingResults = true;
this.api.updateBook(this._id, this.bookForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.router.navigate(['/book-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
Add a function for handling the show article details button.
bookDetails(): void {
this.router.navigate(['/book-details', this._id]);
}
Next, open and edit `src/app/book-edit/book-edit.component.html` then replace all HTML tags with this.
<div class="example-container mat-elevation-z8">
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" (click)="bookDetails()"><mat-icon>info</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="bookForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<input matInput placeholder="ISBN" formControlName="isbn"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('isbn').valid && bookForm.get('isbn').touched">Please enter ISBN</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Title" formControlName="title"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('title').valid && bookForm.get('title').touched">Please enter Title</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Author" formControlName="author"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('author').valid && bookForm.get('author').touched">Please enter Author</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Description" formControlName="description"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('description').valid && bookForm.get('description').touched">Please enter Description</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Publisher" formControlName="publisher"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('publisher').valid && bookForm.get('publisher').touched">Please enter Publisher</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<input matInput placeholder="Published Year" formControlName="publishedYear"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!bookForm.get('publishedYear').valid && bookForm.get('publishedYear').touched">Please enter Published Year</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!bookForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Finally, open and edit `src/app/book-edit/book-edit.component.scss` then add this lines of SCSS codes.
/* Structure */
.example-container {
position: relative;
padding: 5px;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.example-full-width:nth-last-child(0) {
margin-bottom: 10px;
}
.button-row {
margin: 10px 0;
}
.mat-flat-button {
margin: 5px;
}
Step #6: Deploy, Run, and Test the Angular Universal SSR on VPS
Before installing PM2 and Nginx on your VPS, make sure you have installed the latest recommended Node.js and NPM on your VPS. After that, install PM2 using this command.
sudo npm install pm2@latest -g
Next, build the Angular Universal application as SSR.
npm run build:ssr --prod
Next, upload the angular-ssr/dist/angular-ssr/server/main.js to your VPS then move it to /var/www/. Then run this Angular SSR application using PM2.
pm2 start /var/www/main.js --name angular-ssr
And here's the PM2 status look like.
To stop the PM2 type this command.
pm2 stop angular-ssr
Next, to make the PM2 start and run the app every time server starting type this command.
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
`ubuntu` is your server username. To check the status of the startup system, type this command.
systemctl status pm2-ubuntu
You can see this log or output console of the PM2 service.
Next, install Nginx for your VPS operating system then edit the nginx.conf or /etc/nginx/sites-available/default file.
sudo nano /etc/nginx/sites-available/default
Replace the location body with these lines.
location / {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
Restart your Nginx server.
sudo service nginx restart
Now, open your browser then go to http://your_ip_address (e.g: http://192.168.0.7), and here's the application look like.
If there's an error caused by CORS because of using the application using port 80 instead of its origin port 4000. Just install the cors module.
npm install cors @types/cors
Open and edit server.ts then add this import.
import * as cors from 'cors';
Enable cors by adding this line after another server.use.
server.use(cors());
Also, change the API URL in src/app/api.service.ts to this.
const apiUrl = 'http://192.168.0.7/book';
Rebuild again this Angular 10 Universal application then deploy it to your VPS. Stop and start again PM2, and you will not see the CORS error.
That it's, the Angular 10 Universal Server Side Rendering (SSR) CRUD Example. You can find full 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:
- Master en JavaScript: Aprender JS, jQuery, Angular 8, NodeJS
- Angular 8 - Complete Essential Guide
- Learn Angular 8 by creating a simple Full Stack Web App
- Angular 5 Bootcamp FastTrack
- Angular 6 - Soft & Sweet
- Angular 6 with TypeScript
Thanks!