1. Schematic representation
As we can see in the diagram above when the user initiates the navigation we need to check if the component where he navigates away from is protected by a guard, if not we just proceed with the navigation.
When the component is protected by a guard we need to check if the component can be deactivated, if all checks pass we can proceed with the navigation, if a check fails we need to ask the user for confirmation before we proceed with or cancel the navigation.
2. create an interface
We need a way for our guard to ask the component if it can be deactivated, we can achieve this by implementing a function in our component and listen to the returned value of that function in our guard. the cleanest way to do this is by creating an interface.
import {Observable} from 'rxjs
export interface ComponentCanDeactivate {
canDeactivate: () => boolean | Observable<boolean>
}
3. create the guard
We can now create a can-deactivate guard that uses this interface and, since we are only interested in if or not a component can be deactivated, returns an observable<boolean>
import {Injectable} from '@angular/core';
import {CanDeactivate} from '@angular/router';
import {ComponentCanDeactivate} from '../interfaces/can-deactivate.interface';
import {Observable, of} from 'rxjs';
import {first} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class PendingChangesGuard implements CanDeactivate {
constructor() {
}
canDeactivate(component: ComponentCanDeactivate): Observable {
if (!!component.canDeactivate()) {
// component can be deactivated
return of(true);
}
// component can't be deactivated trigger confirm dialog
}
}
This guard doesn’t do much yet so let’s proceed with creating a service where we can trigger the confirmation dialog and that can let us know if or not the user has confirmed the navigation
4. create a service
We need 2 states, 1 to tell the application that it should open a dialog and 1 to let our guard know what the decision of the user was, also we need 4 methods to toggle these states.
import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class PendingChangesService {
confirmed$: BehaviorSubject = new BehaviorSubject(null);
askForConfirmation$: BehaviorSubject = new BehaviorSubject(false);
openConfirmation(): void {
// cancel possible previous confirmation states
this.confirmed$.next(null);
// trigger opening dialog
this.askForConfirmation$.next(true);
}
closeConfirmation(): void {
// cancel possible previous confirmation states
this.confirmed$.next(null);
// close dialog
this.askForConfirmation$.next(false);
}
confirm(): void {
// confirm navigation
this.confirmed$.next(true);
// close dialog
this.askForConfirmation$.next(false);
}
cancel(): void {
// cancel navigation
this.confirmed$.next(false);
// close dialog
this.askForConfirmation$.next(false);
}
}
5. Connect our guard with the service
Now we have our service, our way of communicating, we can start connecting the dots.
First, we update our guard
import {Injectable} from '@angular/core';
import {CanDeactivate} from '@angular/router';
import {ComponentCanDeactivate} from '../interfaces/can-deactivate.interface';
import {PendingChangesService} from '../services/pending-changes.service';
import {Observable, of} from 'rxjs';
import {first} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class PendingChangesGuard implements CanDeactivate {
constructor(private service: PendingChangesService) {
}
canDeactivate(component: ComponentCanDeactivate): Observable {
if (!!component.canDeactivate()) {
this.service.closeConfirmation();
return of(true);
}
this.service.openConfirmation();
return this.service.confirmed$.pipe(
first(v => v !== null)
) as Observable;
}
}
Second, we update our route with our guard.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {Route1Component} from './components/route1/route1.component';
import {Route2Component} from './components/route2/route2.component';
import {PendingChangesGuard} from './guards/pending-changes.guard';
const routes: Routes = [
{path: '/', component: Route1Component},
{path: 'route2', component: Route2Component, canDeactivate: [PendingChangesGuard]}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Next, we also need to update our component, we need to add our canDeactivate function to the component. to avoid mistakes and errors we can do this by implementing our interface.
import {Component} from '@angular/core';
import {ComponentCanDeactivate} from '../../interfaces/can-deactivate.interface';
import {Observable, of} from 'rxjs';
@Component({
selector: 'app-protected',
templateUrl: './protected.component.html',
styleUrls: ['./protected.component.scss']
})
export class ProtectedComponent implements ComponentCanDeactivate {
constructor() {
}
canDeactivate(): boolean | Observable {
// provide component specific logic to decide if component can or can't be deactivated
return of(true);
}
}
Last, we need to create our dialog, I didn’t include the code for the dialog component, this would take us too far from the purpose of this post.
<app-confirmation-dialog
[open]="openDialog$ | async" (confirm) = "confirm()" (cancel)="cancel()" > </app-confirmation-dialog>
import {Component} from '@angular/core';
import {Observable} from 'rxjs';
import {PendingChangesService} from './services/pending-changes.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'pendingChanges';
openDialog$: Observable = this.pendingChangesService.askForConfirmation$;
constructor(private pendingChangesService: PendingChangesService) {
}
confirm(): void {
this.pendingChangesService.confirm();
}
cancel(): void {
this.pendingChangesService.cancel();
}
}
Wrap up
We can now prevent our users from losing data while navigating our app thus saving tons of frustration. Thanks to the easily used interface we only need to add the guard to the route and implement our interface in every component we want to protect, also we left the discussion if or not to deactivate completely within the component self, giving us the opportunity to write custom rules per component.
neat right?
Thanks for reading!