angulartypescriptroutescomponentsguard

How to implement CanDeactivate guard for Angular component loaded by ViewContainerRef?


I designed my Angular page with buttons that route to the same component, called ParentComponent. Each of these buttons passes different query parameter. The ParentComponent has a ViewContainerRef that programmatically loads a child component depending on the query parameter passed by the buttons. For example, if "cmp1" is passed as parameter, it will load Child1Component. If "cmp2" is passed, it will load Child2Component. I defined this mapping in a ComponentModels constants class.

Now, each of the child components has an IsDirty state (and other internal conditions), which when True, should show a confirmation dialog when the user tries to leaves or navigates away from the page. In principle, this mechanism can be achieved if I implement a CanDeactivate guard, but I don't know where to position it.

My questions are:

  1. Which component should implement the CanDeactivate guard? Should it be in ParentComponent or the child components? From what I understand, CanDeactivate only works for those components that are defined in the routing module. In my case, only the ParentComponent is defined in the router module.
  2. If ParentComponent implements the guard, how will it be able to get the IsDirty state of the loaded child component? Each child component has different conditions to decide whether it can really be deactivated.

I'm new to Angular so I'm not sure if my design is complex or incorrect. Any inputs or advise will be much appreciated. Thanks!

MainComponent.html

<p>This is the Main Page</p>
<button (click)="openChild1()">Open Child 1</button>
<button (click)="openChild2()">Open Child 2</button>

MainComponent.ts

export class MainComponent {
  constructor(private router: Router) {}

  openChild1() {
    this.router.navigate(['child', 'cmp1']);
  }

  openChild2() {
    this.router.navigate(['child', 'cmp2']);
  }
}

AppRouting.module.ts

import { MainComponent } from './components/main/main.component';
import { ParentComponent } from './components/parent/parent.component';

const routes: Routes = [
  { path: '', component: MainComponent },
  { path: 'child/:id', component: ParentComponent, canDeactivate: [CanDeactivateGuard]},
];

@NgModule({
  imports: [CommonModule],
  declarations: [],
})
export class AppRoutingModule {}

ParentComponent.html

<h1>Parent View</h1>
<div #container></div>

ParentComponent.ts

import { Component, OnInit } from '@angular/core';
import { ChildComponents } from '../../models/child-components';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css'],
})
export class ParentComponent implements OnInit,  {
  @ViewChild('container', { read: ViewContainerRef })
  container!: ViewContainerRef;

  constructor(
    private readonly route: ActivatedRoute,
    private readonly router: Router
  ) {}

  ngAfterViewInit() {
    this.route.paramMap.subscribe((params) => {
      let id = params.get('id');
      if (id) {
        this.loadComponent(ChildComponents[id]);
      }
    });
  }

  loadComponent(component: any) {
    this.container.createComponent(component);
  }

  canDeactivate(): boolean | Promise<boolean> {
    // should ParentComponent implement canDeactivate?
    // how can it get the IsDirty state of the loaded child component?

    return true;
  }
}

Component-Models.ts

import { Child1Component } from '../components/child1/child1.component';
import { Child2Component } from '../components/child2/child2.component';

export const ComponentModels: { [key: string]: any } = {
  cmp1: Child1Component,
  cmp2: Child2Component,
};

CanDeactivate.guard.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Promise<boolean>;
}

@Injectable({
  providedIn: 'root'
})

export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

Child1Component.ts

export class Child1Component implements CanComponentDeactivate {
  isDirty: boolean;

  constructor() {
  }

  canDeactivate(): boolean | Promise<boolean> {
    //is this correct?
    return this.isDirty === true;
  }
}

Child2Component.ts

export class Child1Component implements CanComponentDeactivate {
  isDirty: boolean;

  constructor() {
  }

  canDeactivate(): boolean | Promise<boolean> {
    //is this correct?
    return this.isDirty === true && conditionX && conditionY;
  }
}

Solution

  • We will get the instance access when we run create component, so you can just access the canDeactivate function from the instance and validate the dirty state.

    The parent is the place where the canDeactivate gets called since it's the component specified in the routing:

    import { Component, OnInit } from '@angular/core';
    import { ChildComponents } from '../../models/child-components';
    
    @Component({
      selector: 'app-parent',
      templateUrl: './parent.component.html',
      styleUrls: ['./parent.component.css'],
    })
    export class ParentComponent implements OnInit,  {
      @ViewChild('container', { read: ViewContainerRef })
      container!: ViewContainerRef;
      componentRef: ComponentRef<any>; // <- changed here!
    
      constructor(
        private readonly route: ActivatedRoute,
        private readonly router: Router
      ) {}
    
      ngAfterViewInit() {
        this.route.paramMap.subscribe((params) => {
          let id = params.get('id');
          if (id) {
            this.loadComponent(ChildComponents[id]);
          }
        });
      }
    
      loadComponent(component: any) {
        this.componentRef = this.container.createComponent(component); // <- changed here!
      }
    
      canDeactivate(): boolean | Promise<boolean> {
        return this.componentRef?.instance?.canDeactivate(); // <- changed here!
      }
    }