Angular
canDeactivate
Modal Dialog
JavaScript
Web Development

How to make Angular canDeactivate Service wait for Modal Dialog response?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Angular's canDeactivate route guard prevents navigation away from a component when there are unsaved changes. To show a confirmation modal and wait for the user's response, the guard must return an Observable<boolean> or Promise<boolean> instead of a synchronous boolean. The modal service emits the user's choice (confirm or cancel), and the guard subscribes to it, allowing or blocking navigation based on the response.

The Guard Interface

typescript
1// can-deactivate.guard.ts
2import { Injectable } from '@angular/core';
3import { CanDeactivate } from '@angular/router';
4import { Observable } from 'rxjs';
5
6export interface CanComponentDeactivate {
7  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
8}
9
10@Injectable({ providedIn: 'root' })
11export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
12  canDeactivate(component: CanComponentDeactivate): Observable<boolean> | Promise<boolean> | boolean {
13    return component.canDeactivate ? component.canDeactivate() : true;
14  }
15}

The guard delegates to the component's canDeactivate() method, which decides whether to show a modal.

The Modal Confirmation Service

typescript
1// confirm-dialog.service.ts
2import { Injectable } from '@angular/core';
3import { MatDialog } from '@angular/material/dialog';
4import { Observable } from 'rxjs';
5import { map } from 'rxjs/operators';
6import { ConfirmDialogComponent } from './confirm-dialog.component';
7
8@Injectable({ providedIn: 'root' })
9export class ConfirmDialogService {
10  constructor(private dialog: MatDialog) {}
11
12  confirm(message: string = 'You have unsaved changes. Discard them?'): Observable<boolean> {
13    const dialogRef = this.dialog.open(ConfirmDialogComponent, {
14      data: { message },
15      disableClose: true,  // Prevent closing by clicking outside
16    });
17
18    return dialogRef.afterClosed().pipe(
19      map(result => result === true)  // true = confirmed, anything else = cancelled
20    );
21  }
22}

afterClosed() returns an Observable that emits once when the dialog closes, then completes. The guard receives this Observable and waits for the user's response.

The Confirmation Dialog Component

typescript
1// confirm-dialog.component.ts
2import { Component, Inject } from '@angular/core';
3import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
4
5@Component({
6  selector: 'app-confirm-dialog',
7  template: `
8    <h2 mat-dialog-title>Unsaved Changes</h2>
9    <mat-dialog-content>{{ data.message }}</mat-dialog-content>
10    <mat-dialog-actions align="end">
11      <button mat-button (click)="dialogRef.close(false)">Cancel</button>
12      <button mat-raised-button color="warn" (click)="dialogRef.close(true)">Discard</button>
13    </mat-dialog-actions>
14  `,
15})
16export class ConfirmDialogComponent {
17  constructor(
18    public dialogRef: MatDialogRef<ConfirmDialogComponent>,
19    @Inject(MAT_DIALOG_DATA) public data: { message: string }
20  ) {}
21}

The Component with Unsaved Changes

typescript
1// edit-form.component.ts
2import { Component } from '@angular/core';
3import { Observable } from 'rxjs';
4import { CanComponentDeactivate } from './can-deactivate.guard';
5import { ConfirmDialogService } from './confirm-dialog.service';
6
7@Component({
8  selector: 'app-edit-form',
9  template: `
10    <form [formGroup]="form">
11      <input formControlName="name" placeholder="Name">
12      <textarea formControlName="bio" placeholder="Bio"></textarea>
13      <button (click)="save()">Save</button>
14    </form>
15  `,
16})
17export class EditFormComponent implements CanComponentDeactivate {
18  form = this.fb.group({
19    name: [''],
20    bio: [''],
21  });
22
23  private saved = false;
24
25  constructor(
26    private fb: FormBuilder,
27    private confirmDialog: ConfirmDialogService
28  ) {}
29
30  save(): void {
31    // Save logic...
32    this.saved = true;
33  }
34
35  canDeactivate(): Observable<boolean> | boolean {
36    if (this.saved || !this.form.dirty) {
37      return true;  // No unsaved changes — allow navigation
38    }
39    // Show modal and wait for response
40    return this.confirmDialog.confirm('You have unsaved changes. Discard them?');
41  }
42}

Route Configuration

typescript
1// app-routing.module.ts
2import { CanDeactivateGuard } from './can-deactivate.guard';
3
4const routes: Routes = [
5  {
6    path: 'edit/:id',
7    component: EditFormComponent,
8    canDeactivate: [CanDeactivateGuard],
9  },
10];

Without Angular Material (Custom Modal)

If you are not using Material, create a simple modal with a Subject:

typescript
1@Injectable({ providedIn: 'root' })
2export class SimpleConfirmService {
3  private response$ = new Subject<boolean>();
4
5  showConfirm = false;
6  message = '';
7
8  confirm(message: string): Observable<boolean> {
9    this.message = message;
10    this.showConfirm = true;
11    return this.response$.asObservable().pipe(take(1));
12  }
13
14  respond(value: boolean): void {
15    this.showConfirm = false;
16    this.response$.next(value);
17  }
18}
html
1<!-- In app.component.html -->
2<div class="modal-overlay" *ngIf="confirmService.showConfirm">
3  <div class="modal">
4    <p>{{ confirmService.message }}</p>
5    <button (click)="confirmService.respond(false)">Cancel</button>
6    <button (click)="confirmService.respond(true)">Discard</button>
7  </div>
8</div>

Angular 16+ Functional Guard

Angular 16 introduced functional guards as an alternative to class-based guards:

typescript
1import { CanDeactivateFn } from '@angular/router';
2
3export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
4  return component.canDeactivate ? component.canDeactivate() : true;
5};
6
7// In routes
8const routes: Routes = [
9  {
10    path: 'edit/:id',
11    component: EditFormComponent,
12    canDeactivate: [canDeactivateGuard],
13  },
14];

Common Pitfalls

  • Returning a synchronous boolean when a modal is needed: If canDeactivate() returns true or false before the modal opens, navigation proceeds immediately without waiting. Return an Observable<boolean> that completes when the user responds.
  • Not using take(1) or completing the Observable: The router subscribes to the Observable and waits for it to emit. If your Observable never emits (e.g., the Subject is not triggered), navigation hangs indefinitely. Use take(1) to ensure exactly one emission.
  • Modal appearing on every navigation: Check form.dirty or a similar flag before showing the modal. If the form has not been modified, return true immediately without opening the dialog.
  • Browser back button bypassing the guard: The canDeactivate guard handles in-app navigation. For browser back/refresh/close, also listen to the beforeunload event: @HostListener('window:beforeunload', ['$event']) unloadNotification(event) { if (this.form.dirty) event.returnValue = true; }.
  • disableClose not set on the dialog: Without disableClose: true, clicking the backdrop closes the dialog and emits undefined, which the guard interprets as false (navigation blocked). Either set disableClose or handle undefined explicitly in the map operator.

Summary

  • The canDeactivate guard returns Observable<boolean> to wait for the modal response
  • The modal service opens a dialog and returns dialogRef.afterClosed() as the Observable
  • The component checks form.dirty before showing the modal — skip if no changes
  • Register the guard in the route configuration with canDeactivate: [CanDeactivateGuard]
  • Add a beforeunload listener for browser back/refresh/close events
  • Use take(1) on custom Subject-based modals to prevent hanging navigation

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.