In this Angular 8 tutorial you will learn how to build Angular 8 CRUD Web App as the frontend, and use existing Node, Express.js, and MongoDB RESTful API as the backend.
Just clone and run the RESTful API backend here or you can use your existing backend/REST API with JSON format for this Angular 8 tutorial.
We will not describe the new Angular 8 feature here because the official Angular.io blog has explained very well. As usual, we will show you a practical walkthrough from the zero to the complete application.
The following tools, frameworks, and modules are required for this tutorial:
We assume that you have installed Node.js. Now, we need to check the Node.js and NPM versions. Open the terminal or Node command line then type this commands.
node -v
v10.15.1
npm -v
6.9.0
That’s the Node.js and NPM version that we are using. Now, you can go to the main steps.
If you are in an existing Angular 7 application, you can update the application using this command form your Angular 7 root directory.
cd angular7-crud
ng update @angular/cli @angular/core
Now, you will see the new version in dependencies in package.json
except for @angular/material
and @angular/cdk
.
{
...
"dependencies": {
"@angular/animations": "~8.0.2",
"@angular/cdk": "^7.0.0",
"@angular/common": "~8.0.2",
"@angular/compiler": "~8.0.2",
"@angular/core": "~8.0.2",
"@angular/forms": "~8.0.2",
"@angular/material": "^7.0.0",
"@angular/platform-browser": "~8.0.2",
"@angular/platform-browser-dynamic": "~8.0.2",
"@angular/router": "~8.0.2",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8",
"rxjs": "~6.5.2",
"tslib": "^1.9.0",
"zone.js": "~0.9.1"
},
...
}
To update Angular Material and CDK, first, you have to uninstall them manually.
npm uninstall --save @angular/material
npm uninstall --save @angular/cdk
Then install them again using Angular 8 CLI.
ng add @angular/material
Choose your default theme during installation progress.
? Choose a prebuilt theme name, or "custom" for a custom theme:
❯ Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ]
Deep Purple/Amber [ Preview: https://material.angular.io?theme=deeppurple-amber ]
Pink/Blue Grey [ Preview: https://material.angular.io?theme=pink-bluegrey ]
Purple/Green [ Preview: https://material.angular.io?theme=purple-green ]
Custom
Leave the other question as default by type Y
. Now if you check the package.json
the Angular Material and CDK version updated to 8.0.1
. Next, we have to run the updated Angular 8 application after running the MongoDB and Node/Express.js API.
ng serve
Oops, there’s something wrong with the CSS.
ERROR in ./src/app/product-add/product-add.component.scss
Module build failed (from ./node_modules/sass-loader/lib/loader.js):
.example-full-width:nth-last-child() {
^
Expected "n".
╷
17 │ .example-full-width:nth-last-child(){
│ ^
╵
stdin 17:36 root stylesheet
Just change all SCSS code that contains nth-last-child()
with this.
nth-last-child(0)
You can find the reference about this here
https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child. Now, you can see the updated Angular 8 application performance in the browser by going to [http://localhost:4200
](http://localhost:4200`) and feel the performance difference with the previous version.
Now, for the new Angular 8 just type this Angular 8 CLI command.
ng new angular8-crud
If you get the question like below, choose Yes
and SCSS
(or whatever you like to choose).
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS
Next, go to the newly created Angular 8 project folder.
cd angular8-crud
Type this command to run the Angular 8 application using this command.
ng serve
Open your browser then go to this address localhost:4200
, you should see this Angular 8 page.
The Angular 8 routes already added when we create new Angular 8 application in the previous step. Routes use to navigate between components. Before configuring the routes, type this command to create a new Angular 8 components.
ng g component products
ng g component product-detail
ng g component product-add
ng g component product-edit
Open src/app/app.module.ts
then you will see those components imported and declared in @NgModule
declarations. Next, open and edit src/app/app-routing.module.ts
then add these imports.
import { ProductsComponent } from './products/products.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
import { ProductAddComponent } from './product-add/product-add.component';
import { ProductEditComponent } from './product-edit/product-edit.component';
Add these arrays to the existing routes constant.
const routes: Routes = [
{
path: 'products',
component: ProductsComponent,
data: { title: 'List of Products' }
},
{
path: 'product-details/:id',
component: ProductDetailComponent,
data: { title: 'Product Details' }
},
{
path: 'product-add',
component: ProductAddComponent,
data: { title: 'Add Product' }
},
{
path: 'product-edit/:id',
component: ProductEditComponent,
data: { title: 'Edit Product' }
},
{ path: '',
redirectTo: '/products',
pathMatch: 'full'
}
];
Open and edit src/app/app.component.html
and you will see existing router outlet. Next, modify this HTML page to fit the CRUD page.
![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==)
Open and edit src/app/app.component.scss
then replace all SASS codes with this.
.container {
padding: 20px;
}
To access RESTful 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 RESTful API emitted by Observable that can subscribe and read from the Components. 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/product.ts
then add these lines of Typescript codes.
export class Product {
_id: string;
prod_name: string;
prod_desc: string;
prod_price: number;
updated_at: 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.
import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
import { Product } from './product';
Add these constants before the @Injectable
.
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
const apiUrl = "/api/v1/products";
Inject HttpClient
module to the constructor.
constructor(private http: HttpClient) { }
Add the error handler function.
private handleError (operation = 'operation', result?: T) {
return (error: any): Observable => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
Add the functions for all CRUD (create, read, update, delete) RESTful call of products data.
getProducts(): Observable {
return this.http.get(apiUrl)
.pipe(
tap(product => console.log('fetched products')),
catchError(this.handleError('getProducts', []))
);
}
getProduct(id: number): Observable {
const url = `${apiUrl}/${id}`;
return this.http.get(url).pipe(
tap(_ => console.log(`fetched product id=${id}`)),
catchError(this.handleError(`getProduct id=${id}`))
);
}
addProduct(product: Product): Observable {
return this.http.post(apiUrl, product, httpOptions).pipe(
tap((prod: Product) => console.log(`added product w/ id=${product.id}`)),
catchError(this.handleError('addProduct'))
);
}
updateProduct(id: any, product: Product): Observable {
const url = `${apiUrl}/${id}`;
return this.http.put(url, product, httpOptions).pipe(
tap(_ => console.log(`updated product id=${id}`)),
catchError(this.handleError('updateProduct'))
);
}
deleteProduct(id: any): Observable {
const url = `${apiUrl}/${id}`;
return this.http.delete(url, httpOptions).pipe(
tap(_ => console.log(`deleted product id=${id}`)),
catchError(this.handleError('deleteProduct'))
);
}
You can find more details about Angular 8 Observable and RXJS here.
We will display the list of products published from API Service. The data published from the API service read by subscribing as a Product model in the Angular 8 component. For that, open and edit src/app/products/products.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/products/products.component.ts
then add these imports.
import { Product } from '../product';
Declare the variables of Angular 8 Material Table Data Source before the constructor.
displayedColumns: string[] = ['prod_name', 'prod_price'];
data: Product[] = [];
isLoadingResults = true;
Modify the ngOnInit
function to get list of products immediately.
ngOnInit() {
this.api.getProducts()
.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/products/products.component.html
then replace all HTML tags with this Angular Material tags.
<div class="example-loading-shade"
*ngIf="isLoadingResults">
add
<table mat-table [dataSource]="data" class="example-table"
matSort matSortActive="prod_name" matSortDisableClear matSortDirection="asc">
Product Name
{{row.prod_name}}
Product Price
$ {{row.prod_price}}
Finally, to make a little UI adjustment, open and edit src/app/products/products.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;
}
If you don’t want to use SASS for styling, rename the file extension to SCSS if the generated style file using SASS. Then change in the src/app/products/products.component.ts
@Component
declarations.
@Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
To show product details after click or tap on the one of a row inside the Angular 8 Material table, open and edit src/app/product-detail/product-detail.component.ts
then add these imports.
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Product } from '../product';
Inject above modules to the constructor.
constructor(private route: ActivatedRoute, private api: ApiService, private router: Router) { }
Declare the variables before the constructor for hold product data that get from the API.
product: Product = { _id: '', prod_name: '', prod_desc: '', prod_price: null, updated_at: null };
isLoadingResults = true;
Add a function for getting Product data from the API.
getProductDetails(id: any) {
this.api.getProduct(id)
.subscribe((data: any) => {
this.product = data;
console.log(this.product);
this.isLoadingResults = false;
});
}
Call that function when the component is initiated.
ngOnInit() {
this.getProductDetails(this.route.snapshot.params['id']);
}
Add this function for delete product.
deleteProduct(id: any) {
this.isLoadingResults = true;
this.api.deleteProduct(id)
.subscribe(res => {
this.isLoadingResults = false;
this.router.navigate(['/products']);
}, (err) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
For the view, open and edit src/app/product-detail/product-detail.component.html
then replace all HTML tags with this.
<div class="example-loading-shade"
*ngIf="isLoadingResults">
list
## {{product.prod_name}}
{{product.prod_desc}}
Product Price:
{{product.prod_price}}
Updated At:
{{product.updated_at | date}}
edit
delete
Finally, open and edit src/app/product-detail/product-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;
}
If the style file extension is not SCSS, do the same way as previous steps.
To create a form for adding a Product, open and edit src/app/product-add/product-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';
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.
productForm: FormGroup;
prod_name = '';
prod_desc = '';
prod_price: number = null;
isLoadingResults = false;
Add initial validation for each field.
ngOnInit() {
this.productForm = this.formBuilder.group({
'prod_name' : [null, Validators.required],
'prod_desc' : [null, Validators.required],
'prod_price' : [null, Validators.required]
});
}
Create a function for submitting or POST product form.
onFormSubmit() {
this.isLoadingResults = true;
this.api.addProduct(this.productForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.router.navigate(['/product-details', 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 main class.
matcher = new MyErrorStateMatcher();
Next, open and edit src/app/product-add/product-add.component.html
then replace all HTML tags with this.
<div class="example-loading-shade"
*ngIf="isLoadingResults">
list
<input matInput placeholder="Product Name" formControlName="prod_name"
[errorStateMatcher]="matcher">
Please enter Product Name
<input matInput placeholder="Product Desc" formControlName="prod_desc"
[errorStateMatcher]="matcher">
Please enter Product Description
<input matInput placeholder="Product Price" formControlName="prod_price"
[errorStateMatcher]="matcher">
Please enter Product Price
save
Finally, open and edit src/app/product-add/product-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;
}
If the style file extension is not SCSS, do the same way as previous steps.
We have put an edit button inside the Product Detail component for call Edit page. Now, open and edit src/app/product-edit/product-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';
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 product form before the constructor.
productForm: FormGroup;
_id = '';
prod_name = '';
prod_desc = '';
prod_price: number = null;
isLoadingResults = false;
Next, add validation for all fields when the component is initiated.
ngOnInit() {
this.getProduct(this.route.snapshot.params['id']);
this.productForm = this.formBuilder.group({
'prod_name' : [null, Validators.required],
'prod_desc' : [null, Validators.required],
'prod_price' : [null, Validators.required]
});
}
Create a function for getting product data that filled to each form fields.
getProduct(id: any) {
this.api.getProduct(id).subscribe((data: any) => {
this._id = data._id;
this.productForm.setValue({
prod_name: data.prod_name,
prod_desc: data.prod_desc,
prod_price: data.prod_price
});
});
}
Create a function to update the product changes.
onFormSubmit() {
this.isLoadingResults = true;
this.api.updateProduct(this._id, this.productForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.router.navigate(['/product-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
Add a function for handling the show product details button.
productDetails() {
this.router.navigate(['/product-details', 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 main class.
matcher = new MyErrorStateMatcher();
Next, open and edit src/app/product-edit/product-edit.component.html
then replace all HTML tags with this.
<div class="example-loading-shade"
*ngIf="isLoadingResults">
info
<input matInput placeholder="Product Name" formControlName="prod_name"
[errorStateMatcher]="matcher">
Please enter Product Name
<input matInput placeholder="Product Desc" formControlName="prod_desc"
[errorStateMatcher]="matcher">
Please enter Product Description
<input matInput placeholder="Product Price" formControlName="prod_price"
[errorStateMatcher]="matcher">
Please enter Product Price
save
Finally, open and edit src/app/product-edit/product-edit.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;
}
If the style file extension is not SCSS, do the same way as previous steps.
Let’s prove the performance of the Angular 8 CRUD Web Application. Now, we have to build the Angular 8 application using this command.
ng build --prod
And we have an 877KB size of the Angular 8 application build for production. Next, we have to test the whole application, first, we have to run MongoDB server and Node/Express API in the different terminal.
mongod
nodemon
Then run the Angular 8 application build, simply type this command.
ng serve
That it’s the Angular 8 CRUD Web App. You can find the full source code in our GitHub.
#angular #web-development #node-js #mongodb #express