Angular 15/16 Free Course #4 - Registration Form & Service Methods
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 4
In part 4 of this Angular tutorial series we're going to add a registration form and method to the account service for registering new users.
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-4
folder. If you haven't completed Part 3 (Login Form, Authentication & Route Guard) but want to follow the steps in this part of the course you can start with the code in the part-3
folder of the GitHub repo.
Tutorial Steps
- Add Register Route to Fake Backend
- Add Register Method to Account Service
- Add Form Logic to Register Component
- Add Form HTML to Register Component Template
- Add Success Alert To Login Component
- Start Angular Application!
Add Register Route to Fake Backend
To simulate the behaviour of a real API with a database, the users
array will be saved in browser local storage and initialized from local storage when the app starts, so registered users will persist when the browser is refreshed or closed. The default hardcoded user (test
) has been removed because you will now register before logging in to the application.
The register route (/users/register
) has been added to the handleRoute()
function, requests to the register route are handled by the new register()
function.
The register()
function:
- checks if the username is already taken and returns an error if it is.
- calculates the next
user.id
by adding 1 to the current highest user id (Math.max(...users.map(x => x.id)) + 1
) or defaults to 1 for the first user. - pushes the new user to the
users
array and saves the updated array in local storage. - returns an empty
ok
response to indicate that registration was successful.
Update Fake Backend
This is how the fake-backend.ts
file should look after the changes, lines 6-8
have been updated and the new lines are 21-22
and 41-52
.
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();
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();
}
// 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 basicDetails(user: any) {
const { id, username, firstName, lastName } = user;
return { id, username, firstName, lastName };
}
}
}
export const fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true
};
Add Register Method to Account Service
The account service currently handles all HTTP communication with the backend for authentication (login()
& logout()
) and provides access to the currently logged in user.
We're going to extend this functionality with to support user registration. The register()
method accepts a user
object parameter containing the user details from the registration form, it sends a POST
request to the register route on the backend (`${environment.apiUrl}/users/register`
), passing the user
object in the request body.
Update Account Service
This is how the account service (account.service.ts
) should look after the changes, the new register()
method is on lines 44-46
.
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);
}
}
Add Form Logic to Register Component
The register component contains all of the logic for validating the register form and handling form submission.
The constructor()
method:
- specifies the dependencies that are required by the component as parameters, these are automatically injected by the Angular Dependency Injection (DI) system when the component is created.
- checks if the user is already logged in by checking
accountService.userValue
and redirects to the home page if they are.
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.
- creates a new
FormGroup
by callingthis.formBuilder.group()
and assigns it to thethis.form
property. The parameters passed to theFormBuilder
tell it to create four form controls -firstName
,lastName
,username
andpassword
, the form controls are all initialized with empty strings (''
) and set to required with theValidators.required
Angular validator. The password field is also required to have at least 6 characters with the use of the minLength validator (Validators.minLength(6)
).
The f()
getter is a convenience property to enable shorthand access to the register form controls via this.f
in the register component and f
in the register component template that we'll create in the next step.
The onSubmit()
method:
- sets the
this.submitted
property to true to indicate that an attempt has been made to submit the form, this property is used in the register component template to display validation errors only after the first submission has been attempted. - resets the error message to an empty string to remove any alert.
- checks if the form is valid by checking
this.form.invalid
and prevents submission if it is invalid. - sets the
this.loading
property to true before submitting the registration details via the account service, this property is used in the register component template to display a loading spinner and disable the register button. - registers the user by calling the
this.accountService.register()
method and passing it the form data (this.form.value
). The account service returns anObservable
that we.subscribe()
to for the results of the registration. On success the user is redirected to the/account/login
route by callingthis.router.navigate()
, passingregistered=true
as a query parameter so the login page can display a success message. On fail the error message is assigned to thethis.error
property to be displayed by the template and thethis.loading
property is reset back to false.
The call to.pipe(first())
unsubscribes from the observable immediately after the first value is emitted.
Update Register Component
Open the register.component.ts
file and update it with the following TypeScript:
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 } from '@app/_services';
@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
form!: FormGroup;
loading = false;
submitted = false;
error?: string;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService
) {
// 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.error = '';
// 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.router.navigate(['/account/login'], { queryParams: { registered: true }});
},
error: error => {
this.error = error;
this.loading = false;
}
});
}
}
Add Form HTML to Register Component Template
The register component template contains the HTML and Angular template syntax for displaying the form in the browser and binding the form to the properties and methods in the register component above.
The [formGroup]="form"
attribute directive binds the form to the form
property of the register component. When the form is submitted in the browser the [formGroup]
directive emits an ngSubmit
event that triggers the onSubmit()
method of the register component. The (ngSubmit)="onSubmit()"
event binding attribute binds the ngSubmit
event to the onSubmit()
method.
Each form input has a formControlName
attribute which syncs the form input with the FormControl
of the same name in the form
.
The [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }"
attribute directive adds the is-invalid
CSS class to firstName input if the form has been submitted and the firstName is invalid (contains errors) based on the validation rules defined in the register component. The same [ngClass]
directive is used on all of the inputs. The is-invalid
class is part of Bootstrap 5, it makes the border of the input red to indicate it is invalid.
The div with CSS class invalid-feedback
following each input is for displaying validation error messages, the messages appear as red text which are styled by Bootstrap 5. The *ngIf="submitted && f.firstName.errors"
directive renders the firstName validation messages if the form has been submitted and the firstName is invalid, the same *ngIf
directive is used for all input validation errors. Within the invalid-feedback
div is a child div for each specific error message, the *ngIf="f.firstName.errors.required"
renders the message First Name is required
if the firstName input is empty. The Validators.required
validator is attached to the firstName control in the register component above and has a value of true
if the input is empty.
The register button is used to submit the form, the [disabled]="loading"
attribute directive disables the button when the loading
property of the register component is true
, a spinner is also displayed in the button while loading with the use of the *ngIf="loading"
directive. The spinner styling and animation is part of Bootstrap 5.
Lastly the cancel link uses the routerLink="../login"
directive to link back to the /account/login
route. The routerLink
directive uses Angular to navigate between routes using partial page updates. Don't use the standard HTML href
attribute because it results in a full application reload each time you click a link.
Update Register Component Template
Open the register.component.html
file and update it with the following HTML:
<div class="container col-md-6 offset-md-3 mt-5">
<div *ngIf="error" class="alert alert-danger">{{error}}</div>
<div class="card">
<h4 class="card-header">Register</h4>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="mb-3">
<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">
<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 class="mb-3">
<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">
<label class="form-label">Password</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>
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
Register
</button>
<a routerLink="../login" class="btn btn-link">Cancel</a>
</div>
</form>
</div>
</div>
</div>
Add Success Alert To Login Component
On successful registration the user is redirected back to the login route and the alert message 'Registration successful' is displayed.
The success alert is implemented in the login component by checking if the query parameter registered=true
exists in the url, the query parameter is added by the register component when it redirects to the login route after successful registration.
The new code in the ngOnInit()
method checks if the registered
property exists on the this.route.snapshot.queryParams
object, if it does the success message 'Registration successful'
is assigned to the this.success
property to be displayed in the register component template.
The new code in the onSubmit()
method resets both this.success
and this.error
properties to reset the UI when the login form is submitted and prevent both messages from being displayed at the same time.
Update Login Component
This is how the login component should look after adding the success alert logic, the new lines are 14
, 34-37
and 48
.
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 } from '@app/_services'
@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
form!: FormGroup;
loading = false;
submitted = false;
error?: string;
success?: string;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService
) {
// 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]
});
// show success message after registration
if (this.route.snapshot.queryParams.registered) {
this.success = 'Registration successful';
}
}
// convenience getter for easy access to form fields
get f() { return this.form.controls; }
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.error = '';
this.success = '';
// 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.error = error;
this.loading = false;
}
});
}
}
Update Login Component Template
This is how the login component template should look after adding the success alert message, the new code is on line 3
.
<div class="container col-md-6 offset-md-3 mt-5">
<div *ngIf="error" class="alert alert-danger">{{error}}</div>
<div *ngIf="success" class="alert alert-success">{{success}}</div>
<div class="card">
<h4 class="card-header">Login</h4>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="mb-3">
<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">
<label class="form-label">Password</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>
</div>
<div>
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
Login
</button>
<a routerLink="../register" class="btn btn-link">Register</a>
</div>
</form>
</div>
</div>
</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.
Register and login
You can now register a new user and login with the registered username and password.
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 | 55.83 kB |
runtime.js | runtime | 6.54 kB |
| Initial Total | 3.01 MB
Build at: 2023-05-03T00:20:07.050Z - Hash: b2b0f65c2ffb4024 - Time: 8187ms
** 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!