2017年3月7日 星期二

[Angular] Routing Guard

 Angular    Route Guard     Guard routings  


Introduction


In Angular, we can use Route Guard to protect our routing while a user may not has the permission to navigate some of our routes.

In the above case, we use CanActivate to decide whether a route can be activated or not.

In other hand, if we don’t want the user to leave current route, CanDeactivate can help us to detect that routing is changing and stop the leaving. It’s often used to confirm the leaving when current Form data is dirty(Changed).

I will make two examples for both to know CanActivate and CanDeactivate.

Environment


Angular 2.4.0
Angular CLI 1.0.0-beta.31




CanActivate


Here is a sample of routing module.

cms-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
//...
const routes: Routes =
[
    { path: 'Login', component: LoginComponent},
    {
        path: 'Layout', component: LayoutComponent children: [
          { path: 'Index', component: IndexComponent,  },
          { path: 'Product/List', component: ProductListComponent,},
          { path: 'Product/Create', component: ProductCreateComponent},
          { path: 'Product/Edit/:id', component: ProductEditComponent},
          { path: 'Link', component: LinkComponent},
        ]
    }];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule],
    providers: []
})
export class CmsRoutingModule { }



So the scenario is that we wanna guard the “Layout” route and its child routes, which are only available for login users.

First, we have to implement a class: LoginRouteGuard, which will decides if the demand route could be activated or not.

LoginRouteGuard

login-route-guard.ts

import { Injectable } from '@angular/core';
import { Router, ActivatedRoute, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot} from '@angular/router';
import { Observable } from 'rxjs';

import { LoginService } from './services/login.service';

@Injectable()
export class LoginRouteGuard implements CanActivate {

    constructor(
      private router: Router,
      private loginSvc: LoginService) {

      }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

        let currentUri = state.url; //Get current uri

        if (this.loginSvc.checkIsAllowed(currentUri)) {
            return Observable.of(true);
        }
        else {
            this.router.navigate(['/Cms/Login']);
        }
    }
}

Notice that when a user is not allowed to navigate the protected route, he/she will be redirected to login page.


cms-routing.module.ts

Let’s update our routing module to support LoginRouteGuard.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginRouteGuard } from './login-route-guard';
//...


const routes: Routes =
[
    { path: 'Login', component: LoginComponent },
    {
        path: 'Layout', component: LayoutComponent, canActivate: [LoginRouteGuard], children: [
          { path: 'Index', component: IndexComponent, canActivate: [LoginRouteGuard] },
          { path: 'Product/List', component: ProductListComponent, canActivate: [LoginRouteGuard]},
          { path: 'Product/Create', component: ProductCreateComponent, canActivate: [LoginRouteGuard]},
          { path: 'Product/Edit/:id', component: ProductEditComponent, canActivate: [LoginRouteGuard]},
          { path: 'Link', component: LinkComponent},
        ]
    }];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule],
    providers: []
})
export class CmsRoutingModule { }




Demo

Okay, it’s done. Assume that the current user could navigate
/Cms/Layout/Index
But has no permission on
/Cms/Layout/Product/List

Here is a demo.





CanActivateChild

Sure the Angular Route Guard works cool, but we don’t want to set

canActivate: [LoginRouteGuard]

on every child route of “Layout”. So we will apply the LoginRouteGuard to all of them with CanActivateChild.



login-route-guard.ts

import { Injectable } from '@angular/core';
import { Router, ActivatedRoute, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot, CanActivateChild } from '@angular/router';
import { Observable } from 'rxjs';

import { LoginService } from './services/login.service';

@Injectable()
export class LoginRouteGuard implements CanActivate, CanActivateChild {

    //Skip the exist codes...

    canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot){
        return this.canActivate(childRoute, state);
    }
}



cms-routing.module.ts


const routes: Routes =
[
    { path: 'Login', component: LoginComponent },
    {
        path: 'Layout', component: LayoutComponent, canActivate: [LoginRouteGuard],  canActivateChild: [LoginRouteGuard], children: [
          { path: 'Index', component: IndexComponent,  },
          { path: 'Product/List', component: ProductListComponent,},
          { path: 'Product/Create', component: ProductCreateComponent},
          { path: 'Product/Edit/:id', component: ProductEditComponent},
          { path: 'Link', component: LinkComponent},
        ]
}];

//...



CanDeactivate


In the following sample, we are going to implement the InputRouteGuard to inform  users that the form data is changed and confirmed the leaving without saving the dirty form data.


InputRouteGuard

input-route-guard.ts

/// <reference path="../../../../assets/lib-npm/typings/sweetalert.d.ts" />
import { Injectable } from '@angular/core';
import { Router, ActivatedRoute, RouterStateSnapshot, ActivatedRouteSnapshot, CanDeactivate } from '@angular/router';
import { FormBaseComponent } from './../components/form-base/form-base.component';
declare var swal: any;


@Injectable()
export class InputRouteGuard implements CanDeactivate<FormBaseComponent> {

    constructor(private router: Router) {

    }

    canDeactivate(component: FormBaseComponent,
                      route: ActivatedRouteSnapshot,
                      state: RouterStateSnapshot) : Promise<boolean> {

        if (component.form.dirty) {
            return new Promise<boolean>(
                resolve => {
                swal({
                    title: 'Are you sure?',
                    text: 'Unsaved changes will be lost!',
                    type: 'warning',
                    showCancelButton: true,
                    confirmButtonText: 'Yes, ignore it.',
                    cancelButtonText: 'No'
                }).then(function (isOk: boolean) {
                    resolve(isOk);
                }, function (dismiss) {
                    resolve(false);
                })
                });

            } else {
            return new Promise( resolve => resolve(true));
        }

        }
    }

In canDeactivate function, return true for leaving current route, or false to stay.

Also take a look at FormBaseComponent, which is used for inhering by components with form.

form-base.component

import { Component, OnInit, ViewChild} from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
    selector: 'share-form-base',
    template: ''
})
export class FormBaseComponent implements OnInit {

 @ViewChild(NgForm) form: NgForm;

    constructor() { }
    ngOnInit() {
    }
}


form-base.component

import { NgModule } from '@angular/core';
import { Routes, RouterModule, CanDeactivate } from '@angular/router';
import { InputRouteGuard } from './../share/route-guard/input-route-guard';

//...

const routes: Routes =
[
    { path: 'Login', component: LoginComponent, canDeactivate: [InputRouteGuard] },
    {
        path: 'Layout', component: LayoutComponent, canActivate: [LoginRouteGuard],  canActivateChild: [LoginRouteGuard], children: [
          { path: 'Index', component: IndexComponent,  },
          { path: 'Product/List', component: ProductListComponent,},
          { path: 'Product/Create', component: ProductCreateComponent, canDeactivate: [InputRouteGuard]},
          { path: 'Product/Edit/:id', component: ProductEditComponent, canDeactivate: [InputRouteGuard]},
          { path: 'Link', component: LinkComponent},
        ]
    }];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule],
    providers: []
})
export class CmsRoutingModule { }




cms-routing.module.ts

Update the routing to support InputRouteGuard on the following routes:
1.  /Cms/Login
2.  /Cms/Layout/Product/Create
3.  /Cms/Layout/Product/Edit

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { InputRouteGuard } from './../share/route-guard/input-route-guard';

//...

const routes: Routes =
[
    { path: 'Login', component: LoginComponent, canDeactivate: [InputRouteGuard] },
    {
        path: 'Layout', component: LayoutComponent, canActivate: [LoginRouteGuard],  canActivateChild: [LoginRouteGuard], children: [
          { path: 'Index', component: IndexComponent,  },
          { path: 'Product/List', component: ProductListComponent,},
          { path: 'Product/Create', component: ProductCreateComponent, canDeactivate: [InputRouteGuard]},
          { path: 'Product/Edit/:id', component: ProductEditComponent, canDeactivate: [InputRouteGuard]},
          { path: 'Link', component: LinkComponent},
        ]
    }];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule],
    providers: []
})
export class CmsRoutingModule { }





Demo




Reference




沒有留言:

張貼留言