Angular: Prevent the user from accidentally losing data while navigating your app.

Angular: Prevent the user from accidentally losing data while navigating your app.


Remember that time it took quite a bit of time to fill in that form and then lost all that progress by accidentally clicking on a link or navigation item? Remember that frustrating feeling you had there? We would like to protect our users from experiencing that while using our app.

We need to make sure that, when a user wants to navigate away from a page, we check if there are still changes pending to be saved. This can be achieved by leveraging the power of the build-in Angular router guards with our own custom logic.

We will build a solution where the user is notified that there are still some unsaved changes on the page and ask for confirmation to navigate away.


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!