Angular 8 - User Registration and Login Example & Tutorial
Tutorial built with Angular 8.2.14 and Webpack 4.41
Other versions available:
- Angular: Angular 15/16, 14, 10, 9, 7, 6, 2/5
- React: React 18 + Redux, React 17 + Recoil, React 16 Hooks + Redux, React 16 + Redux
- Next.js: Next.js 13 (App Router) + MongoDB, Next.js 13 (Pages Router) + MongoDB, Next.js 13 (Pages Router) + MySQL, Next.js 11
- Vue: Vue 3 + Pinia, Vue 2 + Vuex
- AngularJS: AngularJS
- .NET: Blazor WebAssembly
In this tutorial we'll go through an example of how to build a simple user registration and login system using Angular 8, TypeScript and webpack 4.
The project is available on GitHub at https://github.com/cornflourblue/angular-8-registration-login-example.
Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-8-registration-login-example)
Update History:
- 18 Jan 2020 - Posted video tutorial on how to deploy the Angular app to Azure with an ASP.NET Core backend
- 05 Dec 2019 - Posted video tutorial on how to deploy the Angular app to AWS with a Node.js backend
- 26 Nov 2019 - Updated to Angular 8.2.14
- 10 Jun 2019 - Built with Angular 8.0.0
Running the Angular 8 Login Tutorial Example Locally
The tutorial example uses Webpack 4 to transpile the TypeScript code and bundle the Angular 8 modules together, and the webpack dev server is used as the local web server, to learn more about using webpack with TypeScript you can check out the webpack docs.
- Install NodeJS and NPM from https://nodejs.org/en/download/.
- Download or clone the tutorial project source code from https://github.com/cornflourblue/angular-8-registration-login-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Start the application by running
npm start
from the command line in the project root folder. - Your browser should automatically open at
http://localhost:8080
with the demo Angular 8 login page displayed.
For more info on setting up an Angular development environment see Angular - Setup Development Environment.
Running the Tutorial Example with a Real Backend API
The tutorial uses a fake backend that stores users in HTML5 local storage, to switch to using a real web service simply remove the fake backend providers in the app.module.ts
file below the comment // providers used to create fake backend
.
You can build your own backend api or start with one of the below options:
- To run the auth example with a real backend API built with NodeJS and MongoDB follow the instructions at NodeJS + MongoDB - Simple API for Authentication, Registration and User Management
- For an API built with NodeJS and MySQL follow the instructions at Node.js + MySQL - Simple API for Authentication, Registration and User Management
- For a real backend API built with ASP.NET Core 3.1 follow the instructions at ASP.NET Core 3.1 - Simple API for Authentication, Registration and User Management
Deploying the Angular App to AWS
This video shows how to setup a production ready web server from scratch on AWS, then deploy the example Angular app and configure it to run with a real Node.js + MongoDB backend api. The tutorial used in the video is available at Angular + Node.js on AWS - How to Deploy a MEAN Stack App to Amazon EC2.
Deploying the Angular App to Microsoft Azure
This video shows how to deploy the Angular app to Azure with a real backend api built with ASP.NET Core and an Azure SQL Server database, the full tutorial is available at Angular + .NET Core + SQL on Azure - How to Deploy a Full Stack App to Microsoft Azure.
How to Build the App Step by Step - Video Tutorial Series for Angular 7 or Angular 8
This video tutorial series shows how to build the example Angular application step by step from scratch. The series was created with Angular 7 but the code and steps are the same with Angular 8, you just need to use Angular 8 package versions when setting up the base project.
- Part 1 (Windows) - Setup Development Machine on Windows
- Part 1 (Mac) - Setup Development Machine on Mac
- Part 2 - Create Base Project Structure & Webpack Config
- Part 3 - Add Routing & Multiple Pages
- Part 4 - Login Form, Authentication Service & Route Guard
- Part 5 - Registration Form & User Service
- Part 6 - Home Page & Alert Component
- Part 7 (Optional) - Migrating to an Angular CLI Project
Angular 8 Tutorial Project Structure
The project and code structure of the tutorial mostly follows the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.
The project structure has a folder per feature (home, login & register), while other shared/common code (services, models, content, components & helpers) is placed in folders prefixed with an underscore "_" to easily differentiate between shared code and feature specific code, the prefix also groups shared component folders together at the top of the folder structure.
The index.ts
files in each folder are barrel files that group the exported modules from a folder together so they can be imported using the folder path instead of the full module path and to enable importing multiple modules in a single import (e.g. import { AlertService, UserService, AuthenticationService } from '@/_services'
).
A path alias '@' has been configured in the tsconfig.json and webpack.config.js that maps to the '/src/app' directory. This allows imports to be relative to the '/src/app' folder by prefixing the import path with '@', removing the need to use long relative paths like import MyComponent from '../../../MyComponent'
.
Here's the tutorial project structure:
- src
- app
- _components
- alert.component.html
- alert.component.ts
- index.ts
- _content
- _helpers
- _models
- user.ts
- index.ts
- _services
- home
- home.component.html
- home.component.ts
- index.ts
- login
- login.component.html
- login.component.ts
- index.ts
- register
- app.component.html
- app.component.ts
- app.module.ts
- app.routing.ts
- _components
- index.html
- main.ts
- polyfills.ts
- typings.d.ts
- app
- package.json
- tsconfig.json
- webpack.config.js
Below is all the tutorial project code along with brief descriptions of each file to explain how it all fits together.
Alert Component Template
The alert component template contains the html for displaying alert messages at the top of the page.
<div *ngIf="message" [ngClass]="message.cssClass">{{message.text}}</div>
Alert Component
The alert component passes alert messages to the template whenever a message is received from the alert service. It does this by subscribing to the alert service's getMessage() method which returns an Observable.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { AlertService } from '@/_services';
@Component({ selector: 'alert', templateUrl: 'alert.component.html' })
export class AlertComponent implements OnInit, OnDestroy {
private subscription: Subscription;
message: any;
constructor(private alertService: AlertService) { }
ngOnInit() {
this.subscription = this.alertService.getAlert()
.subscribe(message => {
switch (message && message.type) {
case 'success':
message.cssClass = 'alert alert-success';
break;
case 'error':
message.cssClass = 'alert alert-danger';
break;
}
this.message = message;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Global App LESS/CSS Styles
Global LESS/CSS for styling any part of the application.
// global application styles
a {
cursor: pointer;
}
Auth Guard
The auth guard is used to prevent unauthenticated users from accessing restricted routes, in this example it's used in app.routing.ts to protect the home page route. For more information about angular 2+ route guards you can check out this post on the thoughtram blog.
NOTE: While technically it's possible to bypass this client side authentication check by manually adding a 'currentUser' object to local storage using browser dev tools, this would only give access to the client side routes/components, it wouldn't give access to any real secure data from the server api because a valid authentication token (JWT) is required for this.
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthenticationService } from '@/_services';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private authenticationService: AuthenticationService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const currentUser = this.authenticationService.currentUserValue;
if (currentUser) {
// authorised so return true
return true;
}
// not logged in so redirect to login page with the return url
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
return false;
}
}
Http Error Interceptor
The Error Interceptor intercepts http responses from the api to check if there were any errors. If there is a 401 Unauthorized response the user is automatically logged out of the application, all other errors are re-thrown up to the calling service so an alert can be displayed to the user.
It's implemented using the HttpInterceptor class that was introduced in Angular 4.3 as part of the new HttpClientModule. By extending the HttpInterceptor class you can create a custom interceptor to catch all error responses from the server in a single location.
Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthenticationService } from '@/_services';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
// auto logout if 401 response returned from api
this.authenticationService.logout();
location.reload(true);
}
const error = err.error.message || err.statusText;
return throwError(error);
}))
}
}
Fake Backend Provider
The fake backend provider enables the example to run without a backend / backendless, it uses HTML5 local storage for storing registered user data and provides fake implementations for authentication and CRUD methods, these would be handled by a real api and database in a production application.
It's implemented using the HttpInterceptor class that was introduced in Angular 4.3 as part of the new HttpClientModule. By extending the HttpInterceptor class you can create a custom interceptor to modify http requests before they get sent to the server. In this case the FakeBackendInterceptor intercepts certain requests based on their URL and provides a fake response instead of going to the server.
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, mergeMap, materialize, dematerialize } from 'rxjs/operators';
// array in local storage for registered users
let users = JSON.parse(localStorage.getItem('users')) || [];
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
// wrap in delayed observable to simulate server api call
return of(null)
.pipe(mergeMap(handleRoute))
.pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
.pipe(delay(500))
.pipe(dematerialize());
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 === '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({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
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('users', JSON.stringify(users));
return ok();
}
function getUsers() {
if (!isLoggedIn()) return unauthorized();
return ok(users);
}
function deleteUser() {
if (!isLoggedIn()) return unauthorized();
users = users.filter(x => x.id !== idFromUrl());
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
// helper functions
function ok(body?) {
return of(new HttpResponse({ status: 200, body }))
}
function error(message) {
return throwError({ error: { message } });
}
function unauthorized() {
return throwError({ status: 401, error: { message: 'Unauthorised' } });
}
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
};
JWT Interceptor
The JWT Interceptor intercepts http requests from the application to add a JWT auth token to the Authorization header if the user is logged in.
It's implemented using the HttpInterceptor class that was introduced in Angular 4.3 as part of the new HttpClientModule. By extending the HttpInterceptor class you can create a custom interceptor to modify http requests before they get sent to the server.
Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthenticationService } from '@/_services';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add authorization header with jwt token if available
let currentUser = this.authenticationService.currentUserValue;
if (currentUser && currentUser.token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${currentUser.token}`
}
});
}
return next.handle(request);
}
}
User Model
The user model is a small class that defines the properties of a user. The token
property is used to hold the JWT token that is returned from the api on successful authentication.
export class User {
id: number;
username: string;
password: string;
firstName: string;
lastName: string;
token: string;
}
Alert Service
The alert service enables any component in the application to display alert messages at the top of the page via the alert component.
It has methods for displaying success and error messages, and a getMessage() method that returns an Observable that is used by the alert component to subscribe to notifications for whenever a message should be displayed.
import { Injectable } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { Observable, Subject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AlertService {
private subject = new Subject<any>();
private keepAfterRouteChange = false;
constructor(private router: Router) {
// clear alert messages on route change unless 'keepAfterRouteChange' flag is true
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
if (this.keepAfterRouteChange) {
// only keep for a single route change
this.keepAfterRouteChange = false;
} else {
// clear alert message
this.clear();
}
}
});
}
getAlert(): Observable<any> {
return this.subject.asObservable();
}
success(message: string, keepAfterRouteChange = false) {
this.keepAfterRouteChange = keepAfterRouteChange;
this.subject.next({ type: 'success', text: message });
}
error(message: string, keepAfterRouteChange = false) {
this.keepAfterRouteChange = keepAfterRouteChange;
this.subject.next({ type: 'error', text: message });
}
clear() {
// clear by calling subject.next() without parameters
this.subject.next();
}
}
Authentication Service
The authentication service is used to login and logout of the application, to login it posts the users credentials to the api and checks the response for a JWT token, if there is one it means authentication was successful so the user details including the token are added to local storage.
The logged in user details are stored in local storage so the user will stay logged in if they refresh the browser and also between browser sessions until they logout. If you don't want the user to stay logged in between refreshes or sessions the behaviour could easily be changed by storing user details somewhere less persistent such as session storage which would persist between refreshes but not browser sessions, or in a private variable in the authentication service which would be cleared when the browser is refreshed.
There are two properties exposed by the authentication service for accessing the currently logged in user. The currentUser
observable can be used when you want a component to reactively update when a user logs in or out, for example in the app.component.ts so it can show/hide the main nav bar when the user logs in/out. The currentUserValue
property can be used when you just want to get the current value of the logged in user but don't need to reactively update when it changes, for example in the auth.guard.ts which restricts access to routes by checking if the user is currently logged in.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '@/_models';
@Injectable({ providedIn: 'root' })
export class AuthenticationService {
private currentUserSubject: BehaviorSubject<User>;
public currentUser: Observable<User>;
constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
this.currentUser = this.currentUserSubject.asObservable();
}
public get currentUserValue(): User {
return this.currentUserSubject.value;
}
login(username, password) {
return this.http.post<any>(`${config.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('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
return user;
}));
}
logout() {
// remove user from local storage and set current user to null
localStorage.removeItem('currentUser');
this.currentUserSubject.next(null);
}
}
User Service
The user service contains a standard set of CRUD methods for managing users, it acts as the interface between the Angular application and the backend api.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { User } from '@/_models';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) { }
getAll() {
return this.http.get<User[]>(`${config.apiUrl}/users`);
}
register(user: User) {
return this.http.post(`${config.apiUrl}/users/register`, user);
}
delete(id: number) {
return this.http.delete(`${config.apiUrl}/users/${id}`);
}
}
Home Component Template
The home component template contains html and angular 8 template syntax for displaying a simple welcome message and a list of users with a link to delete any user.
<h1>Hi {{currentUser.firstName}}!</h1>
<p>You're logged in with Angular 8!!</p>
<h3>All registered users:</h3>
<ul>
<li *ngFor="let user of users">
{{user.username}} ({{user.firstName}} {{user.lastName}})
- <a (click)="deleteUser(user.id)" class="text-danger">Delete</a>
</li>
</ul>
Home Component
The home component contains logic for displaying the current user, a list of all users and enables the deletion of users.
The constructor()
method assigns the currentUser
property with the value authenticationService.currentUserValue
so the current user can be displayed in the home component template.
The ngOnInit()
method calls the this.loadAllUsers()
method, which calls userService.getAll()
and assigns the result to the this.users
property so the users can be displayed in the home component template.
The deleteUser()
method calls the userService.delete()
method with the user id to delete. The user service returns an Observable
that we .subscribe()
to for the results of the deletion. On success the users list is refreshed by calling this.loadAllUsers()
. The call to .pipe(first())
unsubscribes from the observable immediately after the first value is emitted.
import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';
import { User } from '@/_models';
import { UserService, AuthenticationService } from '@/_services';
@Component({ templateUrl: 'home.component.html' })
export class HomeComponent implements OnInit {
currentUser: User;
users = [];
constructor(
private authenticationService: AuthenticationService,
private userService: UserService
) {
this.currentUser = this.authenticationService.currentUserValue;
}
ngOnInit() {
this.loadAllUsers();
}
deleteUser(id: number) {
this.userService.delete(id)
.pipe(first())
.subscribe(() => this.loadAllUsers());
}
private loadAllUsers() {
this.userService.getAll()
.pipe(first())
.subscribe(users => this.users = users);
}
}
Login Component Template
The login component template contains a login form with username and password fields. It displays validation messages for invalid fields when the submit button is clicked. The form submit event is bound to the onSubmit() method of the login component.
The component uses reactive form validation to validate the input fields, for more information about angular reactive form validation check out Angular 8 - Reactive Forms Validation Example.
<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="username">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="form-group">
<label for="password">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 class="form-group">
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
<a routerLink="/register" class="btn btn-link">Register</a>
</div>
</form>
Login Component
The login component uses the authentication service to login to the application. If the user is already logged in they are automatically redirected to the home page.
The loginForm: FormGroup
object defines the form controls and validators, and is used to access data entered into the form. The FormGroup is part of the Angular Reactive Forms module and is bound to the login template above with the [formGroup]="loginForm"
directive.
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 { AlertService, AuthenticationService } from '@/_services';
@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
loginForm: FormGroup;
loading = false;
submitted = false;
returnUrl: string;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private authenticationService: AuthenticationService,
private alertService: AlertService
) {
// redirect to home if already logged in
if (this.authenticationService.currentUserValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
// get return url from route parameters or default to '/'
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
}
// convenience getter for easy access to form fields
get f() { return this.loginForm.controls; }
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.loginForm.invalid) {
return;
}
this.loading = true;
this.authenticationService.login(this.f.username.value, this.f.password.value)
.pipe(first())
.subscribe(
data => {
this.router.navigate([this.returnUrl]);
},
error => {
this.alertService.error(error);
this.loading = false;
});
}
}
Register Component Template
The register component template contains a simple registration form with fields for first name, last name, username and password. It displays validation messages for invalid fields when the submit button is clicked. The form submit event is bound to the onSubmit() method of the register component.
The component uses reactive form validation to validate the input fields, for more information about angular reactive form validation check out Angular 8 - Reactive Forms Validation Example.
<h2>Register</h2>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="firstName">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="form-group">
<label for="lastName">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="form-group">
<label for="username">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="form-group">
<label for="password">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 class="form-group">
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Register
</button>
<a routerLink="/login" class="btn btn-link">Cancel</a>
</div>
</form>
Register Component
The register component creates a new user with the user service when the register form is submitted. If the user is already logged in they are automatically redirected to the home page.
The registerForm: FormGroup
object defines the form controls and validators, and is used to access data entered into the form. The FormGroup is part of the Angular Reactive Forms module and is bound to the login template above with the [formGroup]="registerForm"
directive.
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AlertService, UserService, AuthenticationService } from '@/_services';
@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
registerForm: FormGroup;
loading = false;
submitted = false;
constructor(
private formBuilder: FormBuilder,
private router: Router,
private authenticationService: AuthenticationService,
private userService: UserService,
private alertService: AlertService
) {
// redirect to home if already logged in
if (this.authenticationService.currentUserValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.registerForm = 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.registerForm.controls; }
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.registerForm.invalid) {
return;
}
this.loading = true;
this.userService.register(this.registerForm.value)
.pipe(first())
.subscribe(
data => {
this.alertService.success('Registration successful', true);
this.router.navigate(['/login']);
},
error => {
this.alertService.error(error);
this.loading = false;
});
}
}
App Component Template
The app component template is the root component template of the application, it contains the main nav bar which is only displayed for authenticated users, a router-outlet directive for displaying the contents of each view based on the current route, and an alert component for displaying alert messages from anywhere in the application.
<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark" *ngIf="currentUser">
<div class="navbar-nav">
<a class="nav-item nav-link" routerLink="/">Home</a>
<a class="nav-item nav-link" (click)="logout()">Logout</a>
</div>
</nav>
<!-- main app container -->
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-sm-6 offset-sm-3">
<alert></alert>
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>
App Component
The app component is the root component of the application, it defines the root tag of the app as <app></app>
with the selector property of the @Component
decorator.
It subscribes to the currentUser
observable in the authentication service so it can reactively show/hide the main navigation bar when the user logs in/out of the application. I didn't worry about unsubscribing from the observable here because it's the root component of the application, the only time the component will be destroyed is when the application is closed which would destroy any subscriptions as well.
The app component contains a logout()
method which is called from the logout link in the main nav bar above to log the user out and redirect them to the login page.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from './_services';
import { User } from './_models';
import './_content/app.less';
@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent {
currentUser: User;
constructor(
private router: Router,
private authenticationService: AuthenticationService
) {
this.authenticationService.currentUser.subscribe(x => this.currentUser = x);
}
logout() {
this.authenticationService.logout();
this.router.navigate(['/login']);
}
}
App Module
The app module defines the root module of the application along with metadata about the module. For more info about angular modules check out this page on the official docs site.
This is where the fake backend provider is added to the application, to switch to a real backend simply remove the providers located below the comment // providers used to create fake backend
.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
// used to create fake backend
import { fakeBackendProvider } from './_helpers';
import { appRoutingModule } from './app.routing';
import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AlertComponent } from './_components';
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule,
HttpClientModule,
appRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
LoginComponent,
RegisterComponent,
AlertComponent
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
// provider used to create fake backend
fakeBackendProvider
],
bootstrap: [AppComponent]
})
export class AppModule { };
App Routing Module
Routing for the Angular app is configured as an array of Routes
, each component is mapped to a path so the Angular Router knows which component to display based on the URL in the browser address bar. The home route is secured by passing the AuthGuard to the canActivate property of the route.
The Routes
array is passed to the RouterModule.forRoot()
method which creates a routing module with all of the app routes configured, and also includes all of the Angular Router providers and directives such as the <router-outlet></router-outlet>
directive. For more information on Angular Routing and Navigation see https://angular.io/guide/router.
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AuthGuard } from './_helpers';
const routes: Routes = [
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
// otherwise redirect to home
{ path: '**', redirectTo: '' }
];
export const appRoutingModule = RouterModule.forRoot(routes);
Main Index Html File
The main index.html file is the initial page loaded by the browser that kicks everything off. Webpack bundles all of the javascript files together and injects them into the body of the index.html page so the scripts get loaded and executed by the browser.
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<title>Angular 8 User Registration and Login Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- bootstrap css -->
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
</body>
</html>
Main (Bootstrap) File
The main file is the entry point used by angular to launch and bootstrap the application.
import './polyfills';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
Polyfills
Some features used by Angular 8 are not yet supported natively by all major browsers, polyfills are used to add support for features where necessary so your Angular 8 application works across all major browsers.
import 'core-js/features/reflect';
import 'zone.js/dist/zone';
Custom Typings File
A custom typings file is used to declare types that are created outside of your angular application, so the TypeScript compiler is aware of them and doesn't give you errors about unknown types. This typings file contains a declaration for the global config
object that is created by webpack (see webpack.config.js below).
// so the typescript compiler doesn't complain about the global config object
declare var config: any;
npm package.json
The package.json file contains project configuration information including package dependencies which get installed when you run npm install
. Full documentation is available on the npm docs website.
{
"name": "angular-8-registration-login-example",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "https://github.com/cornflourblue/angular-8-registration-login-example.git"
},
"scripts": {
"build": "webpack --mode production",
"start": "webpack-dev-server --mode development --open"
},
"license": "MIT",
"dependencies": {
"@angular/common": "^8.0.0",
"@angular/compiler": "^8.0.0",
"@angular/core": "^8.0.0",
"@angular/forms": "^8.0.0",
"@angular/platform-browser": "^8.0.0",
"@angular/platform-browser-dynamic": "^8.0.0",
"@angular/router": "^8.0.0",
"core-js": "^3.1.3",
"rxjs": "^6.3.3",
"zone.js": "^0.9.1"
},
"devDependencies": {
"@types/node": "^12.0.7",
"angular2-template-loader": "^0.6.2",
"css-loader": "^2.1.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"style-loader": "^0.23.1",
"ts-loader": "^6.0.1",
"typescript": "^3.1.3",
"webpack": "^4.32.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.7.0"
}
}
TypeScript tsconfig.json
The tsconfig.json file configures how the TypeScript compiler will convert TypeScript into JavaScript that is understood by the browser. More information is available on the TypeScript docs.
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES5",
"baseUrl": "src",
"paths": {
"@/*": [
"app/*"
]
}
}
}
Webpack 4 Config
Webpack 4 is used to compile and bundle all the project files so they're ready to be loaded into a browser, it does this with the help of loaders and plugins that are configured in the webpack.config.js file. For more info about webpack check out the webpack docs.
This is a fairly basic webpack.config.js for bundling an Angular 8 application, it:
- compiles Angular TypeScript files using
ts-loader
. - loads angular templates with the
angular2-template-loader
andhtml-loader
. - converts LESS files into CSS and loads them into the application with the
style-loader
,css-loader
andless-loader
. - injects the bundled scripts into the body of the index.html page using the
HtmlWebpackPlugin
. - defines a global config object with the plugin
webpack.DefinePlugin
.
A path alias '@' is configured in the webpack.config.js and the tsconfig.json that maps to the '/src/app' directory. This allows imports to be relative to the '/src/app' folder by prefixing the import path with '@', removing the need to use long relative paths like import MyComponent from '../../../MyComponent'
.
It also includes a couple of workarounds to prevent the following warnings from appearing in the console when running the app: "System.import() is deprecated and will be removed soon. Use import() instead." and "Critical dependency: the request of a dependency is an expression". The warnings themselves are harmless, this is just to prevent them from displaying.
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './src/main.ts',
resolve: {
extensions: ['.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src/app/'),
}
},
module: {
rules: [
{
test: /\.ts$/,
use: ['ts-loader', 'angular2-template-loader']
},
{
test: /\.html$/,
use: 'html-loader'
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
// workaround for warning: System.import() is deprecated and will be removed soon. Use import() instead.
{
test: /[\/\\]@angular[\/\\].+\.js$/,
parser: { system: true }
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new webpack.DefinePlugin({
// global app config object
config: JSON.stringify({
apiUrl: 'http://localhost:4000'
})
}),
// workaround for warning: Critical dependency: the request of a dependency is an expression
new webpack.ContextReplacementPlugin(
/\@angular(\\|\/)core(\\|\/)fesm5/,
path.resolve(__dirname, 'src')
)
],
optimization: {
splitChunks: {
chunks: 'all',
},
runtimeChunk: true
},
devServer: {
historyApiFallback: true
}
}
Need Some Angular 8 Help?
Search fiverr for freelance Angular 8 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!