Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

  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 its 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!
  }
}
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading