Angular 15/16 Free Course #7 - Migrate to Standalone Components and Functional Interceptors
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:
- Part 1 - Create Base Project Structure
- Part 2 - Add Routing & Multiple Pages
- Part 3 - Login Form, Authentication & Route Guard
- Part 4 - Registration Form & Service Methods
- Part 5 - Alerts & Home Page
- Part 6 - User Management (CRUD) Section
- Part 7 - Migrate to Standalone Components and Functional Interceptors
- Part 8 - Dockerize App with Nginx
Angular Tutorial Part 7
In part 7 of this Angular tutorial series we're going to convert our project to use the new standalone components feature.
Angular Standalone Components
Standalone components aim to reduce the amount of boilerplate code required by removing the need to use Angular modules (@NgModule
). Standalone components (and directives + pipes) can be declared by adding the standalone: true
property to the @Component
decorator. Dependencies are added directly to standalone components with an imports
array, removing the need for an Angular module.
Functional Interceptors and Route Guards
We'll also simplify our HTTP interceptors and route guards by converting them from classes to simple functions. It's no longer necessary to create a class that implements the HttpInterceptor
or CanActivate
interface.
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-7
folder. If you haven't completed Part 6 (User Management (CRUD) Section) but want to follow the steps in this part of the course you can start with the code in the part-6
folder of the GitHub repo.
Tutorial Steps
- Update Angular Components to Standalone
- Update to Functional HTTP Interceptors
- Update to Functional Router Guard
- Configure APP_ROUTES and USERS_ROUTES
- Switch to Standalone Bootstrapping in
main.ts
- Delete Old Angular Module (
NgModule
) Files - Start the Angular Standalone Component App
Update Angular Components to Standalone
First we'll update all of our Angular components to be standalone by adding the standalone: true
property to the @Component
decorator and including any dependencies in the imports
array.
It's possible to partly automate this process with an Angular schematic (ng generate @angular/core:standalone
), but we'll be doing it manually to go through all the changes step by step. For more info on migrating with the Angular CLI see https://angular.io/guide/standalone-migration.
Standalone App Component
This is the app component (/src/app/app.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property and the imports
array with all dependencies of the component. The rest of the component is unchanged.
import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { AccountService } from './_services';
import { User } from './_models';
import { AlertComponent } from './_components/alert.component';
@Component({
selector: 'app-root', templateUrl: 'app.component.html',
standalone: true,
imports: [NgIf, RouterOutlet, RouterLink, RouterLinkActive, AlertComponent]
})
export class AppComponent {
user?: User | null;
constructor(private accountService: AccountService) {
this.accountService.user.subscribe(x => this.user = x);
}
logout() {
this.accountService.logout();
}
}
Standalone Alert Component
This is the alert component (/src/app/_components/alert.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property and the imports
array with all dependencies of the component.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { NgIf, NgClass } from '@angular/common';
import { Subscription } from 'rxjs';
import { AlertService } from '@app/_services';
@Component({
selector: 'alert', templateUrl: 'alert.component.html',
standalone: true,
imports: [NgIf, NgClass]
})
export class AlertComponent implements OnInit, OnDestroy {
private subscription!: Subscription;
alert: any;
constructor(private alertService: AlertService) { }
ngOnInit() {
this.subscription = this.alertService.onAlert()
.subscribe(alert => {
switch (alert?.type) {
case 'success':
alert.cssClass = 'alert alert-success';
break;
case 'error':
alert.cssClass = 'alert alert-danger';
break;
}
this.alert = alert;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Standalone Login Component
This is the login component (/src/app/account/login.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property and the imports
array with all dependencies of the component.
import { Component, OnInit } from '@angular/core';
import { NgClass, NgIf } from '@angular/common';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AccountService, AlertService } from '@app/_services'
@Component({
templateUrl: 'login.component.html',
standalone: true,
imports: [ReactiveFormsModule, NgClass, NgIf, RouterLink]
})
export class LoginComponent implements OnInit {
form!: FormGroup;
loading = false;
submitted = false;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService,
private alertService: AlertService
) {
// redirect to home if already logged in
if (this.accountService.userValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.form = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
// 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.loading = true;
this.accountService.login(this.f.username.value, this.f.password.value)
.pipe(first())
.subscribe({
next: () => {
// get return url from query parameters or default to home page
const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
this.router.navigateByUrl(returnUrl);
},
error: error => {
this.alertService.error(error);
this.loading = false;
}
});
}
}
Standalone Register Component
This is the register component (/src/app/account/register.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property and the imports
array with all dependencies of the component.
import { Component, OnInit } from '@angular/core';
import { NgClass, NgIf } from '@angular/common';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AccountService, AlertService } from '@app/_services';
@Component({
templateUrl: 'register.component.html',
standalone: true,
imports: [ReactiveFormsModule, NgClass, NgIf, RouterLink]
})
export class RegisterComponent implements OnInit {
form!: FormGroup;
loading = false;
submitted = false;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService,
private alertService: AlertService
) {
// redirect to home if already logged in
if (this.accountService.userValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.form = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
username: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
// convenience getter for easy access to form fields
get f() { return this.form.controls; }
onSubmit() {
this.submitted = true;
// reset alert on submit
this.alertService.clear();
// stop here if form is invalid
if (this.form.invalid) {
return;
}
this.loading = true;
this.accountService.register(this.form.value)
.pipe(first())
.subscribe({
next: () => {
this.alertService.success('Registration successful', true);
this.router.navigate(['/account/login'], { queryParams: { registered: true }});
},
error: error => {
this.alertService.error(error);
this.loading = false;
}
});
}
}
Standalone Home Component
This is the home component (/src/app/home/home.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property. It doesn't depend on any other components.
import { Component } from '@angular/core';
import { User } from '@app/_models';
import { AccountService } from '@app/_services';
@Component({
templateUrl: 'home.component.html',
standalone: true
})
export class HomeComponent {
user: User | null;
constructor(private accountService: AccountService) {
this.user = this.accountService.userValue;
}
}
Standalone Add/Edit User Component
This is the add/edit user component (/src/app/users/add-edit.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property and the imports
array with all dependencies of the component.
import { Component, OnInit } from '@angular/core';
import { NgIf, NgClass } from '@angular/common';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AccountService, AlertService } from '@app/_services';
@Component({
templateUrl: 'add-edit.component.html',
standalone: true,
imports: [NgIf, ReactiveFormsModule, NgClass, RouterLink]
})
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);
}
}
Standalone List Users Component
This is the list users component (/src/app/users/list.component.ts
) after converting to standalone, the @Component
decorator now has the standalone: true
property and the imports
array with all dependencies of the component.
import { Component, OnInit } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';
import { RouterLink } from '@angular/router';
import { first } from 'rxjs/operators';
import { AccountService } from '@app/_services';
@Component({
templateUrl: 'list.component.html',
standalone: true,
imports: [RouterLink, NgFor, NgIf]
})
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));
}
}
Update to Functional HTTP Interceptors
Next we'll simplify our HTTP interceptors by converting them from classes to simple functions.
Functional JWT Interceptor
This is the JWT interceptor (/src/app/_helpers/jwt.interceptor.ts
) after converting to a simple function. It was previously a class that implemented the HttpInterceptor
interface and had an intercept()
method, the method has been converted into the jwtInterceptor()
function.
We're using the new inject()
function to inject the AccountService
dependency, since there is no longer a class constructor()
to inject dependencies.
import { inject } from '@angular/core';
import { HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { environment } from '@environments/environment';
import { AccountService } from '@app/_services';
export function jwtInterceptor(request: HttpRequest<any>, next: HttpHandlerFn) {
// add auth header with jwt if user is logged in and request is to the api url
const accountService = inject(AccountService);
const user = accountService.userValue;
const isLoggedIn = user?.token;
const isApiUrl = request.url.startsWith(environment.apiUrl);
if (isLoggedIn && isApiUrl) {
request = request.clone({
setHeaders: { Authorization: `Bearer ${user.token}` }
});
}
return next(request);
}
Functional Error Interceptor
This is the HTTP error interceptor (/src/app/_helpers/error.interceptor.ts
) after converting to a simple function. It was previously a class that implemented the HttpInterceptor
interface and had an intercept()
method, the method has been converted into the errorInterceptor()
function.
import { inject } from '@angular/core';
import { HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AccountService } from '@app/_services';
export function errorInterceptor(request: HttpRequest<any>, next: HttpHandlerFn) {
const accountService = inject(AccountService);
return next(request).pipe(catchError(err => {
if ([401, 403].includes(err.status) && accountService.userValue) {
// auto logout if 401 or 403 response returned from api
accountService.logout();
}
const error = err.error?.message || err.statusText;
console.error(err);
return throwError(() => error);
}))
}
Functional Fake Backend Interceptor
This is the fake backend HTTP interceptor (/src/app/_helpers/fake-backend.ts
) after converting to a simple function. It was previously a class that implemented the HttpInterceptor
interface and had an intercept()
method, the method has been converted into the fakeBackendInterceptor()
function.
import { HttpRequest, HttpResponse, HttpHandlerFn } from '@angular/common/http';
import { 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)!) || [];
export function fakeBackendInterceptor(request: HttpRequest<any>, next: HttpHandlerFn) {
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(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]);
}
}
Update to Functional Router Guard
Next we'll simplify our router guard by converting it from a class to a simple functions.
Functional Auth Guard
This is the auth guard (/src/app/_helpers/auth.guard.ts
) after converting to a simple function. It was previously a class that implemented the CanActivate
interface and had a canActivate()
method, the method has been converted into the authGuard()
function.
We're using the new inject()
function to inject the Router
and AccountService
dependencies, since there is no longer a class constructor()
to inject dependencies.
import { inject } from '@angular/core';
import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AccountService } from '@app/_services';
export function authGuard(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const router = inject(Router);
const accountService = inject(AccountService);
const user = accountService.userValue;
if (user) {
// authorised so return true
return true;
}
// not logged in so redirect to login page with the return url
router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } });
return false;
}
Configure APP_ROUTES and USERS_ROUTES
Now we'll simplify our route configuration by converting them from routing modules to simple [feature]_ROUTES
objects.
Users Routes Object
This is the new config file for users routes (/src/app/users/users.routes.ts
). The routes were previously configured in the users routing module (/src/app/users/users-routing.module.ts
) and have been replaced by the simple USERS_ROUTES
object.
import { Routes } from '@angular/router';
import { ListComponent } from './list.component';
import { AddEditComponent } from './add-edit.component';
export const USERS_ROUTES: Routes = [
{ path: '', component: ListComponent },
{ path: 'add', component: AddEditComponent },
{ path: 'edit/:id', component: AddEditComponent }
];
App Routes Object
This is the new config file for the base app routes (/src/app/app.routes.ts
). The routes were previously configured in the app routing module (/src/app/app-routing.module.ts
) and have been replaced by the simple APP_ROUTES
object.
The users routes are lazy loaded with the callback function assigned to loadChildren
in the route config.
import { Routes } from "@angular/router";
import { HomeComponent } from './home';
import { LoginComponent, RegisterComponent } from './account';
import { authGuard } from './_helpers';
const usersRoutes = () => import('./users/users.routes').then(x => x.USERS_ROUTES);
export const APP_ROUTES: Routes = [
{ path: '', component: HomeComponent, canActivate: [authGuard] },
{ path: 'users', loadChildren: usersRoutes, canActivate: [authGuard] },
{ path: 'account/login', component: LoginComponent },
{ path: 'account/register', component: RegisterComponent },
// otherwise redirect to home
{ path: '**', redirectTo: '' }
];
Switch to Standalone Bootstrapping in main.ts
Now we'll update the Angular app to standalone bootstrapping by passing a standalone component to the bootstrapApplication()
function instead of calling platformBrowserDynamic().bootstrapModule(AppModule)
.
Standalone Bootstrapping
This is the updated main.ts
file that bootstraps our standalone AppComponent
to start the Angular app, it previously started the app by bootstrapping the AppModule
.
The providers
array adds support for Router
and HttpClient
functionality to the standalone app. Routes are configured by passing the APP_ROUTES
object to provideRouter()
, and HTTP interceptors are configured by passing the withInterceptors()
function to provideHttpClient()
.
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
// fake backend
import { fakeBackendInterceptor } from '@app/_helpers';
import { AppComponent } from '@app/app.component';
import { jwtInterceptor, errorInterceptor } from '@app/_helpers';
import { APP_ROUTES } from '@app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(APP_ROUTES),
provideHttpClient(
withInterceptors([
jwtInterceptor,
errorInterceptor,
// fake backend
fakeBackendInterceptor
])
)
]
});
Delete Old Angular Module (NgModule
) Files
Finally delete our old Angular modules as they are no longer needed:
- App Module -
/src/app/app.module.ts
- App Routing Module -
/src/app/app-routing.module.ts
- Users Module -
/src/app/users/users.module.ts
- Users Routing Module -
/src/app/users/users-routing.module.ts
Start the Angular Standalone Component App!
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.56 MB |
polyfills.js | polyfills | 328.83 kB |
styles.css, styles.js | styles | 226.27 kB |
main.js | main | 59.00 kB |
runtime.js | runtime | 12.64 kB |
| Initial Total | 3.17 MB
Lazy Chunk Files | Names | Raw Size
src_app_users_users_routes_ts.js | users-users-routes | 27.79 kB |
Build at: 2023-05-16T02:30:52.579Z - Hash: 19d712bf978f2bdd - Time: 11141ms
** 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
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!