Angular 8 CRUD Example
In this tutorial, we will be developing an Angular 8 application and perform CRUD operation on a user entity. We will be developing a full stack app with REST API integration. We will have a login, list user, add and edit user component and based on the routing configurations these pages will be served. We will have HTTP interceptor to intercept our HTTP request and inject JWT token in the header. The backend server code is implemented using Spring Boot.
Angular 8 Release Highlights
With the release of Angular 8, there are many new features that has been introduced such as Ivy Preview, Web Workers, Lazy Loading, Differential Loading, etc. The new version requires TypeScript 3.4+ and Node 12+.
Ivy is a new rendering engine that will produce smaller bundle sizes that is more performance optimised. To start a new project with Ivy enabled, use the --enable-ivy flag with the ng new command.
ng new shiny-ivy-app --enable-ivy
Web Workers allow you to run CPU intensive computations in a background thread, freeing the main thread to update the user interface.
Differential Loading is used to build separate bundles to legacy browsers with related necessary JS bundles and polyfills by default.
Angular 8 Environment Setup
As Angular 8 requires Node 12+, let us download Node 12.5 from the official website and install it. Once Node 12+ is installed, we have NPM 6.9 available.
Now, it's time to update
@angular/cli
. Either you can uninstall the previous version of it and install it again to have the latest version or else try ng update.npm install -g @angular/cli #install latest version of angular/cli ng update @angular/cli #Update to latest version npm uninstall -g @angular/cli #Uninstall the previous version npm cache clean
If you remember, we have already created an Angular 7 CRUD application and Angular 6 CRUD application in our last post. Let us try to upgrade Angular 7 app to Angular 8 in this section. In the below section, we will start building an Angular 8 app from scratch.
Traverse to the working workspace of Angular 7 project and run below update command to upgrade the exisitng application to Angular 8. As our application was a simple app, there may not be any breaking changes.
Angular 8 CLI Project Generation
Let us first generate a sample Angular 8 project through angular/cli and then we will modify it to create a full stack app to perform CRUD operations - list, add, edit and delete user.
Execute below commands to generate an Angular 8 project with CLI. Here, we to try out Ivy, we will be generating the new project through the switch
enable-ivy
ng new angular8-demo --enable-ivy cd angular8-demo/ ng serve
Now, you can actually import this project in your IDE for further changes.
Angular 8 Components
We will have different components for login, add user, edit user and list user. Let us generate these components with CLI commands first.
ng g component login ng g component user/list-user ng g component user/add-user ng g component user/edit-user
Once these components are generated, let us start implementing each components step by step.First, let us remove the default content of app.component.html.
Login Component
The login component has a form to take username and password as input. It uses reactive forms for validation. Username and password is validated from the DB. The server is a spring boot app with JWT authentication. It's source code can be found at github here and the complete implementation of this server project can be found here.
login.component.html
<div class="row"> <div class="col-md-6 login-container"> <h2 style="margin: auto">Login </h2> <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label for="username">UserName:</label> <input type="text" class="form-control" formControlName="username" id="username" autocomplete="off"> <div class="error" *ngIf="loginForm.controls['username'].hasError('required') && loginForm.controls['username'].touched">Username is required</div> </div> <div class="form-group"> <label for="pwd">Password:</label> <input type="password" class="form-control" formControlName="password" id="pwd" autocomplete="off"> <div class="error" *ngIf="loginForm.controls['password'].hasError('required') && loginForm.controls['password'].touched">Password is required</div> </div> <button class="btn btn-success" [disabled]="loginForm.invalid">Login</button> <div *ngIf="invalidLogin" class="error"> <div>Invalid credentials.</div> </div> </form> </div> </div>
Post login, the token will be saved in the local storage.
login.component.ts
import { Component, OnInit } from '@angular/core'; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {Router} from "@angular/router"; import {ApiService} from "../service/api.service"; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { loginForm: FormGroup; invalidLogin: boolean = false; constructor(private formBuilder: FormBuilder, private router: Router, private apiService: ApiService) { } onSubmit() { if (this.loginForm.invalid) { return; } const loginPayload = { username: this.loginForm.controls.username.value, password: this.loginForm.controls.password.value } this.apiService.login(loginPayload).subscribe(data => { debugger; if(data.status === 200) { window.localStorage.setItem('token', data.result.token); this.router.navigate(['list-user']); }else { this.invalidLogin = true; alert(data.message); } }); } ngOnInit() { window.localStorage.removeItem('token'); this.loginForm = this.formBuilder.group({ username: ['', Validators.compose([Validators.required])], password: ['', Validators.required] }); } }
List User Component
Post login, list-user component will be rendered. This component renders the list of users and an option to perform CRUD operations. If there is no valid token found in the localstorage, the user will be redirected to login page again.
You can visit my another article to know Angular JWT authentication in detail.
list-user.component.html
<div class="col-md-6 user-container"> <h2 style="margin: auto"> User Details</h2> <button class="btn btn-danger" style="width:100px" (click)="addUser()"> Add User</button> <table class="table table-striped"> <thead> <tr> <th class="hidden">Id</th> <th>FirstName</th> <th>LastName</th> <th>UserName</th> <th>Age</th> <th>Salary</th> </tr> </thead> <tbody> <tr *ngFor="let user of users"> <td class="hidden"></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td><button class="btn btn-success" (click)="deleteUser(user)"> Delete</button> <button class="btn btn-success" (click)="editUser(user)" style="margin-left: 20px;"> Edit</button></td> </tr> </tbody> </table> </div>list-user.component.html
import { Component, OnInit , Inject} from '@angular/core'; import {Router} from "@angular/router"; import {User} from "../../model/user.model"; import {ApiService} from "../../service/api.service"; @Component({ selector: 'app-list-user', templateUrl: './list-user.component.html', styleUrls: ['./list-user.component.css'] }) export class ListUserComponent implements OnInit { users: User[]; constructor(private router: Router, private apiService: ApiService) { } ngOnInit() { if(!window.localStorage.getItem('token')) { this.router.navigate(['login']); return; } this.apiService.getUsers() .subscribe( data => { this.users = data.result; }); } deleteUser(user: User): void { this.apiService.deleteUser(user.id) .subscribe( data => { this.users = this.users.filter(u => u !== user); }) }; editUser(user: User): void { window.localStorage.removeItem("editUserId"); window.localStorage.setItem("editUserId", user.id.toString()); this.router.navigate(['edit-user']); }; addUser(): void { this.router.navigate(['add-user']); }; }
Add User Component
Let us add new user to our DB with this component.
add-user.component.ts
import { Component, OnInit } from '@angular/core'; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {Router} from "@angular/router"; import {ApiService} from "../../service/api.service"; @Component({ selector: 'app-add-user', templateUrl: './add-user.component.html', styleUrls: ['./add-user.component.css'] }) export class AddUserComponent implements OnInit { constructor(private formBuilder: FormBuilder,private router: Router, private apiService: ApiService) { } addForm: FormGroup; ngOnInit() { this.addForm = this.formBuilder.group({ id: [], username: ['', Validators.required], password: ['', Validators.required], firstName: ['', Validators.required], lastName: ['', Validators.required], age: ['', Validators.required], salary: ['', Validators.required] }); } onSubmit() { this.apiService.createUser(this.addForm.value) .subscribe( data => { this.router.navigate(['list-user']); }); } }add-user.component.html
<div class="col-md-6 user-container"> <h2 class="text-center">Add User</h2> <form [formGroup]="addForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label for="username">User Name:</label> <input type="text" formControlName="username" placeholder="username" name="username" class="form-control" id="username"> </div> <div class="form-group"> <label for="password">Password:</label> <input type="password" formControlName="password" placeholder="password" name="password" class="form-control" id="password"> </div> <div class="form-group"> <label for="firstName">First Name:</label> <input formControlName="firstName" placeholder="First Name" name="firstName" class="form-control" id="firstName"> </div> <div class="form-group"> <label for="lastName">Last Name:</label> <input formControlName="lastName" placeholder="Last name" name="lastName" class="form-control" id="lastName"> </div> <div class="form-group"> <label for="age">Age:</label> <input type="number" formControlName="age" placeholder="age" name="age" class="form-control" id="age"> </div> <div class="form-group"> <label for="salary">Salary:</label> <input type="number" formControlName="salary" placeholder="salary" name="salary" class="form-control" id="salary"> </div> <button class="btn btn-success">Add</button> </form> </div>
Edit User Component
Whenever, edit button is clicked, the selected user id is stored in the local storage and inside
edit-user.component.html
ngOnInit()
, an API call is made to fetch the user by user id and the form is auto-populated.<div class="col-md-6 user-container"> <h2 class="text-center">Edit User</h2> <form [formGroup]="editForm" (ngSubmit)="onSubmit()"> <div class="hidden"> <input type="text" formControlName="id" placeholder="id" name="id" class="form-control" id="id"> </div> <div class="form-group"> <label for="username">User Name:</label> <input type="text" formControlName="username" placeholder="username" name="username" class="form-control" id="username" readonly="true"> </div> <div class="form-group"> <label for="firstName">First Name:</label> <input formControlName="firstName" placeholder="First Name" name="firstName" class="form-control" id="firstName"> </div> <div class="form-group"> <label for="lastName">Last Name:</label> <input formControlName="lastName" placeholder="Last name" name="lastName" class="form-control" id="lastName"> </div> <div class="form-group"> <label for="age">Age:</label> <input type="number" formControlName="age" placeholder="age" name="age" class="form-control" id="age"> </div> <div class="form-group"> <label for="salary">Salary:</label> <input type="number" formControlName="salary" placeholder="salary" name="salary" class="form-control" id="salary"> </div> <button class="btn btn-success">Update</button> </form> </div>edit-user.component.ts
import { Component, OnInit , Inject} from '@angular/core'; import {Router} from "@angular/router"; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {first} from "rxjs/operators"; import {User} from "../../model/user.model"; import {ApiService} from "../../service/api.service"; @Component({ selector: 'app-edit-user', templateUrl: './edit-user.component.html', styleUrls: ['./edit-user.component.css'] }) export class EditUserComponent implements OnInit { user: User; editForm: FormGroup; constructor(private formBuilder: FormBuilder,private router: Router, private apiService: ApiService) { } ngOnInit() { let userId = window.localStorage.getItem("editUserId"); if(!userId) { alert("Invalid action.") this.router.navigate(['list-user']); return; } this.editForm = this.formBuilder.group({ id: [''], username: ['', Validators.required], firstName: ['', Validators.required], lastName: ['', Validators.required], age: ['', Validators.required], salary: ['', Validators.required] }); this.apiService.getUserById(+userId) .subscribe( data => { this.editForm.setValue(data.result); }); } onSubmit() { this.apiService.updateUser(this.editForm.value) .pipe(first()) .subscribe( data => { if(data.status === 200) { alert('User updated successfully.'); this.router.navigate(['list-user']); }else { alert(data.message); } }, error => { alert(error); }); } }
Angular 8 API Service Implementation
Below is the service class that makes HTTP calls to perform the CRUD operations. It uses
api.service.ts
HttpClient
from @angular/common/http
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import {User} from "../model/user.model"; import {Observable} from "rxjs/index"; import {ApiResponse} from "../model/api.response"; @Injectable() export class ApiService { constructor(private http: HttpClient) { } baseUrl: string = 'http://localhost:8080/users/'; login(loginPayload) : Observable<ApiResponse> { return this.http.post<ApiResponse>('http://localhost:8080/' + 'token/generate-token', loginPayload); } getUsers() : Observable<ApiResponse> { return this.http.get<ApiResponse>(this.baseUrl); } getUserById(id: number): Observable<ApiResponse> { return this.http.get<ApiResponse>(this.baseUrl + id); } createUser(user: User): Observable<ApiResponse> { return this.http.post<ApiResponse>(this.baseUrl, user); } updateUser(user: User): Observable<ApiResponse> { return this.http.put<ApiResponse>(this.baseUrl + user.id, user); } deleteUser(id: number): Observable<ApiResponse> { return this.http.delete<ApiResponse>(this.baseUrl + id); } }
Angular 8 Routing
The routing implementation in Angular 8 is similar to previous versions. Below is the implementation of our routing module that is imported in our main module.
app.routing.ts
import { RouterModule, Routes } from '@angular/router'; import {LoginComponent} from "./login/login.component"; import {AddUserComponent} from "./user/add-user/add-user.component"; import {ListUserComponent} from "./user/list-user/list-user.component"; import {EditUserComponent} from "./user/edit-user/edit-user.component"; const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'add-user', component: AddUserComponent }, { path: 'list-user', component: ListUserComponent }, { path: 'edit-user', component: EditUserComponent }, {path : '', component : LoginComponent} ]; export const routing = RouterModule.forRoot(routes);
Angular 8 App Module
We have all the modules such as BrowserModule, ReactiveFormsModule, RoutingModule, etc are importd here and we have bootstrapped AppComponent which provides the starting point of our app.
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { ListUserComponent } from './user/list-user/list-user.component'; import { LoginComponent } from './login/login.component'; import { AddUserComponent } from './user/add-user/add-user.component'; import { EditUserComponent } from './user/edit-user/edit-user.component'; import {routing} from "./app.routing"; import {ReactiveFormsModule} from "@angular/forms"; import {ApiService} from "./service/api.service"; import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http"; import {TokenInterceptor} from "./core/interceptor"; @NgModule({ declarations: [ AppComponent, ListUserComponent, LoginComponent, AddUserComponent, EditUserComponent ], imports: [ BrowserModule, routing, ReactiveFormsModule, HttpClientModule ], providers: [ApiService, {provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi : true}], bootstrap: [AppComponent] }) export class AppModule { }
Angular 8 HTTP Interceptor
The interceptor implements HttpInterceptor which intercepts all the HTTP request and token in the header for API authentication. At the time of login, there won't be any valid token present in the local cache hence, there is a condition check for the presence of token.
interceptor.ts
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; import {Observable} from "rxjs/internal/Observable"; import {Injectable} from "@angular/core"; @Injectable() export class TokenInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable > { let token = window.localStorage.getItem('token'); if (token) { request = request.clone({ setHeaders: { Authorization: 'Bearer ' + token } }); } return next.handle(request); } }
API Details
API Name - Token Generation URL - localhost:8080/token/generate-token Method - POST Header - Content-Type: application/json Body - { "username":"alex123", "password":"alex123" } Response : { "status": 200, "message": "success", "result": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGV4MTIzIiwic2NvcGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vZGV2Z2xhbi5jb20iLCJpYXQiOjE1NDEwNjIzOTMsImV4cCI6MTU0MTA4MDM5M30.DMoB5kv72X7Jf-U5APdjK3UUcGomA9NuJj6XGxmLyqE", "username": "alex123" } } API Name - List User URL - http://localhost:8080/users Method - Get Header - Content-Type: application/json Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGV4MTIzIiwic2NvcGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vZGV2Z2xhbi5jb20iLCJpYXQiOjE1NDEwNjIzOTMsImV4cCI6MTU0MTA4MDM5M30.DMoB5kv72X7Jf-U5APdjK3UUcGomA9NuJj6XGxmLyqE Response - { "status": 200, "message": "User list fetched successfully.", "result": [ { "id": 1, "firstName": "Devglan", "lastName": "Devglan", "username": "devglan", "salary": 3456, "age": 33 }, { "id": 2, "firstName": "Tom", "lastName": "Asr", "username": "tom234", "salary": 7823, "age": 23 }, { "id": 3, "firstName": "Adam", "lastName": "Psr", "username": "adam", "salary": 4234, "age": 45 } ] } API Name - Create User URL - http://localhost:8080/users Method - POST Header - Content-Type: application/json Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGV4MTIzIiwic2NvcGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vZGV2Z2xhbi5jb20iLCJpYXQiOjE1NDEwNjIzOTMsImV4cCI6MTU0MTA4MDM5M30.DMoB5kv72X7Jf-U5APdjK3UUcGomA9NuJj6XGxmLyqE Body - { "username":"test", "password":"test", "firstName":"test", "lastName":"test", "age":23, "salary":12345 } Response - { "status": 200, "message": "User saved successfully.", "result": { "id": 4, "firstName": "test", "lastName": "test"", "username": "test", "salary": 12345, "age": 23 } } API Name - Update User URL - http://localhost:8080/users/4 Method - PUT Header - Content-Type: application/json Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGV4MTIzIiwic2NvcGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vZGV2Z2xhbi5jb20iLCJpYXQiOjE1NDEwNjIzOTMsImV4cCI6MTU0MTA4MDM5M30.DMoB5kv72X7Jf-U5APdjK3UUcGomA9NuJj6XGxmLyqE Body - { "username":"test1", "password":"test1", "firstName":"test1", "lastName":"test1", "age":24, "salary":12345 } Response - { "status": 200, "message": "User updated successfully.", "result": { "id": 0, "firstName": "test1", "lastName": "test1", "username": "test1", "password": "test1", "age": 24, "salary": 12345 } } API Name - Delete User URL - http://localhost:8080/users/4 Method - DELETE Header - Content-Type: application/json Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGV4MTIzIiwic2NvcGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FETUlOIn1dLCJpc3MiOiJodHRwOi8vZGV2Z2xhbi5jb20iLCJpYXQiOjE1NDEwNjIzOTMsImV4cCI6MTU0MTA4MDM5M30.DMoB5kv72X7Jf-U5APdjK3UUcGomA9NuJj6XGxmLyqE Response - { "status": 200, "message": "User deleted successfully.", "result": null }
Comments
Post a Comment