angular2: CanDeactivate guard

Luis Abreu picture Luis Abreu · Dec 14, 2016 · Viewed 16.4k times · Source

I've created a CanDeactivate guard which returns an observable and it's applied to a component which is loaded in a inner nested router-outlet. Should this guard be called whenever one tries to navigate to another url? I'm asking this because this is not happening in my case.

In my case, the guard will only get called for the first "different" URL. Let me try to explain it with an example. Assume I'm always returning false and I'm trying to navigate to different urls from the same component:

/A --> guard called
/B --> guard called
/B --> no navigation and no guard called
/A --> guard called
/A -->guard not called and no navigation

Is this the expected behavior?

edit Well, it seems like it is. Have just built a small sample with 3 components and the guard will only be called for the 1st time the user tries to navigate to a specific url...this is really weird...

Anyways, here's the code I'm using:

// app.routing
import {NgModule} from "@angular/core";
import {Routes, RouterModule, Route, CanDeactivate, ActivatedRouteSnapshot, 
        RouterStateSnapshot} from "@angular/router";
import { MainComponent } from "./main/main.component";
import { OtherComponent } from "./other/other.component";
import { Other3Component } from "./other3/other3.component";
import {Observable} from "rxjs/observable";
const fallback: Route = {
    path: "**",
    redirectTo: "/main",
    pathMatch: "full"
};
export class Test implements CanDeactivate<MainComponent>{
  canDeactivate(component: MainComponent, route: ActivatedRouteSnapshot, 
            state: RouterStateSnapshot): Observable<boolean> | boolean{
    console.log("in");
    return false;
  }
}
export const rotas: Routes = [
{
    path: "main",
    component: MainComponent,
    canDeactivate: [Test]
},
{
    path: "other",
    component: OtherComponent
},
{
    path: "other3",
    component: Other3Component
},
fallback
];

@NgModule({
 imports: [RouterModule.forRoot(rotas)],
 exports: [RouterModule]
})
export class AppRoutingModule{}

//app.component.html <h1> <a routerLink="/main">Main</a> <a routerLink="/other">Other</a> <a routerLink="/other3">Other3</a> </h1>

Everything was generated through angular-cli (ex.: n g component XXX). Yes, the CanDeactivate guard will always return false so you won't be able to unload the main component. So, the first time I click other, the guard gets called. If click again on other, no guard gets called. However, if I click over other3, then the guard gets called. Clicking over other3 won't really do anything until I've clicked on other link (ex.: other)...

Is this the expected behavior? I must say that I expected my guard to get hit everytime I hit another link...

Thanks.

Luis

Answer

Abdelhalim FELLAGUE CHEBRA picture Abdelhalim FELLAGUE CHEBRA · Apr 19, 2017

i have found this solution, instead of creating a candeactivate guard for every component, you will create one guard service and add a candeactivate method to every component you want to add this option, so first you have to add this service file "deactivate-guard.service.ts":

import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';

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

@Injectable()
export class DeactivateGuardService implements  CanDeactivate<CanComponentDeactivate>{

  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

then you have to provide in the app module:

providers: [
    DeactivateGuardService
  ]

now in the component you want to protect, add the function:

export class ExampleComponent {
    loading: boolean = false;
    //some behaviour that change the loading value
    canDeactivate() {
        console.log('i am navigating away');
        if (this.loading) {
            console.log('no, you wont navigate anywhere');
            return false;
        }
        console.log('you are going away, goodby');
        return true;
    }
}

you can see that the variable loading is local to the component. the final step is to add the directive to the component in the routing module:

{ 
  path: 'example', 
  canDeactivate: [DeactivateGuardService],
  component: ExampleComponent 
}

and thats it, i hope this was helpfull, goodluck.