Published: May 10 2023
Last updated: May 11 2023

Angular 15/16 Free Course #6 - User Management (CRUD) Section

Built and tested with Angular 15 and Angular 16

In this free step by step Angular course we'll be covering how to implement routing, authentication, registration and CRUD functionality in Angular.

Other parts available in this Angular course:


Angular Tutorial Part 6

In part 6 of this Angular tutorial series we're going to add a user management section with CRUD (Create, Read, Update, Delete) functionality.

Code on GitHub

The complete source code for this part of the tutorial is available on GitHub at https://github.com/cornflourblue/angular-16-tutorial in the part-6 folder. If you haven't completed Part 5 (Alerts & Home Page) but want to follow the steps in this part of the course you can start with the code in the part-5 folder of the GitHub repo.


Tutorial Steps

  1. Add CRUD Routes to Fake Backend API
  2. Add CRUD Methods to Account Service
  3. Create User Management Pages
  4. Create Users Feature Module
  5. Update Nav with Link to Users Section
  6. Start Angular Application!


Add CRUD Routes to Fake Backend API

To support CRUD functionality we'll add routes to the fake backend API for getting, updating and deleting users.

The getUsers() function:

  • checks if the user is logged in, if not a 401 Unauthorized response is returned.
  • returns an array of all users.

The getUserById() function:

  • checks if the user is logged in, if not a 401 Unauthorized response is returned.
  • returns the user with the specified id.

The updateUser() function:

  • checks if the user is logged in, if not a 401 Unauthorized response is returned.
  • updates the specified user with the params from the PUT request body.
  • returns an empty 200 OK response

The deleteUser() function:

  • checks if the user is logged in, if not a 401 Unauthorized response is returned.
  • deletes the user with the specified id.
  • returns an empty 200 OK response

Update Fake Backend

This is how the fake backend (/src/_helpers/fake-backend.ts) should look with the new CRUD routes, the new lines are 23-30, 62-98, 112-115 and 122-129.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, materialize, dematerialize } from 'rxjs/operators';

// array in local storage for registered users
const usersKey = 'angular-tutorial-users';
let users: any[] = JSON.parse(localStorage.getItem(usersKey)!) || [];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        return handleRoute();

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/users/register') && method === 'POST':
                    return register();
                case url.endsWith('/users') && method === 'GET':
                    return getUsers();
                case url.match(/\/users\/\d+$/) && method === 'GET':
                    return getUserById();
                case url.match(/\/users\/\d+$/) && method === 'PUT':
                    return updateUser();
                case url.match(/\/users\/\d+$/) && method === 'DELETE':
                    return deleteUser();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }    
        }

        // route functions

        function authenticate() {
            const { username, password } = body;
            const user = users.find(x => x.username === username && x.password === password);
            if (!user) return error('Username or password is incorrect');
            return ok({
                ...basicDetails(user),
                token: 'fake-jwt-token'
            })
        }

        function register() {
            const user = body

            if (users.find(x => x.username === user.username)) {
                return error('Username "' + user.username + '" is already taken')
            }

            user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
            users.push(user);
            localStorage.setItem(usersKey, JSON.stringify(users));
            return ok();
        }

        function getUsers() {
            if (!isLoggedIn()) return unauthorized();
            return ok(users.map(x => basicDetails(x)));
        }

        function getUserById() {
            if (!isLoggedIn()) return unauthorized();

            const user = users.find(x => x.id === idFromUrl());
            return ok(basicDetails(user));
        }

        function updateUser() {
            if (!isLoggedIn()) return unauthorized();

            let params = body;
            let user = users.find(x => x.id === idFromUrl());

            // only update password if entered
            if (!params.password) {
                delete params.password;
            }

            // update and save user
            Object.assign(user, params);
            localStorage.setItem(usersKey, JSON.stringify(users));

            return ok();
        }

        function deleteUser() {
            if (!isLoggedIn()) return unauthorized();

            users = users.filter(x => x.id !== idFromUrl());
            localStorage.setItem(usersKey, JSON.stringify(users));
            return ok();
        }

        // helper functions

        function ok(body?: any) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500)); // delay observable to simulate server api call
        }

        function error(message: string) {
            return throwError(() => ({ error: { message } }))
                .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
        }

        function unauthorized() {
            return throwError(() => ({ status: 401, error: { message: 'Unauthorized' } }))
                .pipe(materialize(), delay(500), dematerialize());
        }

        function basicDetails(user: any) {
            const { id, username, firstName, lastName } = user;
            return { id, username, firstName, lastName };
        }

        function isLoggedIn() {
            return headers.get('Authorization') === 'Bearer fake-jwt-token';
        }

        function idFromUrl() {
            const urlParts = url.split('/');
            return parseInt(urlParts[urlParts.length - 1]);
        }
    }
}

export const fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};


Add CRUD Methods to Account Service

Next we'll add CRUD methods to the account service to send HTTP requests to the backend API to get, update and delete users.

The getAll() method:

  • sends a GET request to the /users API route to fetch all users.

The getById(id: string) method:

  • sends a GET request to the /users/{id} API route to fetch the user with the specified id.

The update(id: string, params: any) method:

  • sends a PUT request to the /users/{id} API route to update the specified user with the provided params.
  • if the logged in user updated there own record, the current user object is updated in local storage and published to subscriber components with this.userSubject.next(user).

The delete(id: string) method:

  • sends a DELETE request to the /users/{id} API route to delete the user with the specified id.
  • if the logged in user deleted there own record they are automatically logged out of the app.

Update Account Service

This is how the account service (/src/_services/account.service.ts) should look after adding the CRUD methods, the new lines are 48-81.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { User } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class AccountService {
    private userSubject: BehaviorSubject<User | null>;
    public user: Observable<User | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
        this.user = this.userSubject.asObservable();
    }

    public get userValue() {
        return this.userSubject.value;
    }

    login(username: string, password: string) {
        return this.http.post<User>(`${environment.apiUrl}/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('user', JSON.stringify(user));
                this.userSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem('user');
        this.userSubject.next(null);
        this.router.navigate(['/account/login']);
    }

    register(user: User) {
        return this.http.post(`${environment.apiUrl}/users/register`, user);
    }

    getAll() {
        return this.http.get<User[]>(`${environment.apiUrl}/users`);
    }

    getById(id: string) {
        return this.http.get<User>(`${environment.apiUrl}/users/${id}`);
    }

    update(id: string, params: any) {
        return this.http.put(`${environment.apiUrl}/users/${id}`, params)
            .pipe(map(x => {
                // update stored user if the logged in user updated their own record
                if (id == this.userValue?.id) {
                    // update local storage
                    const user = { ...this.userValue, ...params };
                    localStorage.setItem('user', JSON.stringify(user));

                    // publish updated user to subscribers
                    this.userSubject.next(user);
                }
                return x;
            }));
    }

    delete(id: string) {
        return this.http.delete(`${environment.apiUrl}/users/${id}`)
            .pipe(map(x => {
                // auto logout if the logged in user deleted their own record
                if (id == this.userValue?.id) {
                    this.logout();
                }
                return x;
            }));
    }
}


Create User Management Pages

The user management pages will provide the interface for performing CRUD operations on users in the Angular app.

Create Users Folder

Create a users folder inside the app folder. This will hold all components for the users feature.

List Users Component

The list users component contains the logic for fetching all users from the API and deleting users.

The constructor() method:

  • specifies the account service as a dependency by including it as a parameter, this is automatically injected by the Angular Dependency Injection (DI) system when the component is created.

The ngOnInit() method:

  • is an Angular lifecycle hook that runs once after the component is created. For more info on Angular lifecycle hooks see https://angular.io/guide/lifecycle-hooks.
  • fetches all users from the API by calling this.accountService.getAll() and assigns the response to the users component property.
    The call to .pipe(first()) unsubscribes from the observable immediately after the first value is emitted.

The deleteUser() method:

  • gets the specified user from the users array.
  • sets the user.isDeleting property to true, this is used in the component template to disable the delete button and show a spinner while the user is deleting.
  • deletes the specified user by calling this.accountService.delete(id).
  • removes the deleted user from the users array so it is removed from the UI.

Create List Users Component

Create a file named list.component.ts inside the users folder and add the following TypeScript code to it:

import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'list.component.html' })
export class ListComponent implements OnInit {
    users?: any[];

    constructor(private accountService: AccountService) {}

    ngOnInit() {
        this.accountService.getAll()
            .pipe(first())
            .subscribe(users => this.users = users);
    }

    deleteUser(id: string) {
        const user = this.users!.find(x => x.id === id);
        user.isDeleting = true;
        this.accountService.delete(id)
            .pipe(first())
            .subscribe(() => this.users = this.users!.filter(x => x.id !== id));
    }
}


List Users Component Template

The list users component template displays a list of all users and contains buttons for adding, editing and deleting users.

An *ngFor directive loops over the array of users to render a table row for each.

The add and edit buttons link to different routes with the routerLink directive.

The delete button is bound to the deleteUser() method with the (click)="deleteUser(user.id)" event binding attribute, the [disabled]="user.isDeleting" attribute directive disables the button when the isDeleting property of the user is true, a spinner is also displayed in the button while deleting with the use of the *ngIf="user.isDeleting" directive. The spinner styling and animation is part of Bootstrap 5.

 

Create List Users Component Template

Create a file named list.component.html inside the users folder and add the following HTML to it:

<div class="p-4">
    <div class="container">
        <h1>Users</h1>
        <a routerLink="add" class="btn btn-sm btn-success mb-2">Add User</a>
        <table class="table table-striped">
            <thead>
                <tr>
                    <th style="width: 30%">First Name</th>
                    <th style="width: 30%">Last Name</th>
                    <th style="width: 30%">Username</th>
                    <th style="width: 10%"></th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let user of users">
                    <td>{{user.firstName}}</td>
                    <td>{{user.lastName}}</td>
                    <td>{{user.username}}</td>
                    <td style="white-space: nowrap">
                        <a routerLink="edit/{{user.id}}" class="btn btn-sm btn-primary me-1">Edit</a>
                        <button (click)="deleteUser(user.id)" class="btn btn-sm btn-danger" style="width: 58px" [disabled]="user.isDeleting">
                            <span *ngIf="user.isDeleting" class="spinner-border spinner-border-sm"></span>
                            <span *ngIf="!user.isDeleting">Delete</span>
                        </button>
                    </td>
                </tr>
                <tr *ngIf="!users">
                    <td colspan="4" class="text-center">
                        <span class="spinner-border spinner-border-lg align-center"></span>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>


Add/Edit User Component

The add/edit user component is used for both adding and editing users, the component is in edit mode when there a user id route parameter, otherwise it is in add mode.

In add mode the password field is required and the form fields are empty by default. In edit mode the password field is optional and the form is pre-populated with the specified user details, which are fetched from the API with the account service.

On submit a user is either created or updated by calling the account service, and on success you are redirected back to the users list page with a success message.

Create Add/Edit Component

Create a file named add-edit.component.ts inside the users folder and add the following TypeScript code to it:

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';

@Component({ templateUrl: 'add-edit.component.html' })
export class AddEditComponent implements OnInit {
    form!: FormGroup;
    id?: string;
    title!: string;
    loading = false;
    submitting = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.id = this.route.snapshot.params['id'];

        // form with validation rules
        this.form = this.formBuilder.group({
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            username: ['', Validators.required],
            // password only required in add mode
            password: ['', [Validators.minLength(6), ...(!this.id ? [Validators.required] : [])]]
        });

        this.title = 'Add User';
        if (this.id) {
            // edit mode
            this.title = 'Edit User';
            this.loading = true;
            this.accountService.getById(this.id)
                .pipe(first())
                .subscribe(x => {
                    this.form.patchValue(x);
                    this.loading = false;
                });
        }
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.submitting = true;
        this.saveUser()
            .pipe(first())
            .subscribe({
                next: () => {
                    this.alertService.success('User saved', true);
                    this.router.navigateByUrl('/users');
                },
                error: error => {
                    this.alertService.error(error);
                    this.submitting = false;
                }
            })
    }

    private saveUser() {
        // create or update user based on id param
        return this.id
            ? this.accountService.update(this.id!, this.form.value)
            : this.accountService.register(this.form.value);
    }
}


Add/Edit User Component Template

The add/edit user component template contains a dynamic form that supports both adding and editing users. The form is in edit mode when there a user id property in the current route, otherwise it is in add mode.

In edit mode the form is pre-populated with user details fetched from the API and the password field is optional. The dynamic behaviour is implemented in the add/edit user component above.

Create Add/Edit User Component Template

Create a file named add-edit.component.html inside the users folder and add the following HTML to it:

<div class="p-4">
    <div class="container">
        <h1>{{title}}</h1>
        <form *ngIf="!loading" [formGroup]="form" (ngSubmit)="onSubmit()">
            <div class="row">
                <div class="mb-3 col">
                    <label class="form-label">First Name</label>
                    <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
                    <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
                        <div *ngIf="f.firstName.errors.required">First Name is required</div>
                    </div>
                </div>
                <div class="mb-3 col">
                    <label class="form-label">Last Name</label>
                    <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
                    <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
                        <div *ngIf="f.lastName.errors.required">Last Name is required</div>
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="mb-3 col">
                    <label class="form-label">Username</label>
                    <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
                    <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
                        <div *ngIf="f.username.errors.required">Username is required</div>
                    </div>
                </div>
                <div class="mb-3 col">
                    <label class="form-label">
                        Password
                        <em *ngIf="id">(Leave blank to keep the same password)</em>
                    </label>
                    <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                    <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                        <div *ngIf="f.password.errors.required">Password is required</div>
                        <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
                    </div>
                </div>
            </div>
            <div class="mb-3">
                <button [disabled]="submitting" class="btn btn-primary">
                    <span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span>
                    Save
                </button>
                <a routerLink="/users" class="btn btn-link">Cancel</a>
            </div>
        </form>
        <div *ngIf="loading" class="text-center m-5">
            <span class="spinner-border spinner-border-lg align-center"></span>
        </div>
    </div>
</div>


Create Users Feature Module

We're going to wrap the user management pages and routes in a self-contained Angular feature module. Feature modules can be lazy loaded to improve performance by only loading when requested by the user.

Component names only need to be unique within the feature module, so we can use more concise names without worrying about clashing with other components in the Angular app (e.g. ListComponent and AddEditComponent instead of UsersListComponent and UsersAddEditComponent).

For more info about angular feature modules see https://angular.io/guide/feature-modules.

Users Routing Module

The users routing module defines the routes for the users feature module. It includes routes for listing, adding and editing users.

The add and edit routes are different but both load the same component (AddEditComponent) which modifies its behaviour based on the route.

Create Users Routing Module

Create a file named users-routing.module.ts inside the users folder and add the following TypeScript code to it:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ListComponent } from './list.component';
import { AddEditComponent } from './add-edit.component';

const routes: Routes = [
    { path: '', component: ListComponent },
    { path: 'add', component: AddEditComponent },
    { path: 'edit/:id', component: AddEditComponent }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class UsersRoutingModule { }


Users Feature Module

The users module defines the feature module for the users section of the tutorial application along with metadata about the module. The imports specify which other angular modules are required by this module, and the declarations state which components belong to this module.

The users module is hooked into the main app inside the app routing module with lazy loading in the next step.

Create Users Feature Module

Create a file named users.module.ts inside the users folder and add the following TypeScript code to it:

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { UsersRoutingModule } from './users-routing.module';
import { ListComponent } from './list.component';
import { AddEditComponent } from './add-edit.component';

@NgModule({
    imports: [
        CommonModule,
        ReactiveFormsModule,
        UsersRoutingModule
    ],
    declarations: [
        ListComponent,
        AddEditComponent
    ]
})
export class UsersModule { }


Lazy Load Users Feature Module in App Routing Module

The users feature module is lazy loaded into the Angular app by configuring a route with the loadChildren property and passing it a callback function that dynamically imports the UsersModule when requested.

Update App Routing Module

Open the app routing module file (/src/app/app-routing.module.ts) and add a lazy loaded route for the users module.

This is how the app routing module file should look after the update, the new lines are 8 and 12.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';
import { AuthGuard } from './_helpers';

const usersModule = () => import('./users/users.module').then(x => x.UsersModule);

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'users', loadChildren: usersModule, canActivate: [AuthGuard] },
    { path: 'account/login', component: LoginComponent },
    { path: 'account/register', component: RegisterComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }


Update Nav with Link to Users Section

Open the app component template (/src/app/app.component.html) and add a link to the new users section.

Also add routerLinkActive="active" to each link to highlight the current nav link with the CSS class active. The [routerLinkActiveOptions]="{exact: true}" option only highlights the home link when the path exactly matches the routerLink (/), otherwise it matches any path that begins with the link path (e.g. /users*).

This is how the app component template should look after the update, line 4 is updated and line 5 is new.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="user">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
        <a class="nav-item nav-link" routerLink="/users" routerLinkActive="active">Users</a>
        <button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button>
    </div>
</nav>

<!-- main app container -->
<div class="container">
    <alert></alert>
    <router-outlet></router-outlet>
</div>


Start Angular Application!

Run the command npm start from the project root folder (where the package.json is located) to start the Angular application, then open a browser tab to the URL http://localhost:4200.

The command output should look something like this:

PS C:\Projects\angular-tutorial> npm start

> [email protected] start
> ng serve

✔ Browser application bundle generation complete.

Initial Chunk Files              | Names              |  Raw Size
vendor.js                        | vendor             |   2.44 MB |
polyfills.js                     | polyfills          | 314.29 kB |
styles.css, styles.js            | styles             | 209.42 kB |
main.js                          | main               |  64.83 kB |
runtime.js                       | runtime            |  12.65 kB |

                                 | Initial Total      |   3.02 MB

Lazy Chunk Files                 | Names              |  Raw Size
src_app_users_users_module_ts.js | users-users-module |  30.84 kB |

Build at: 2023-05-10T02:12:05.631Z - Hash: d6d8a273a0d84dfc - Time: 13322ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **


√ Compiled successfully.

 


Need Some Angular 16 Help?

Search fiverr for freelance Angular 16 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by