Angular Routing and Guards

routing guards

This article on Routing in Angular is part of the Learning Angular series.

In the app, we will have 3 sections:
1. Home
2. Servers
– View and Edit Servers
– A service is used to load and update Servers
3. Users
– View users.

Setting Up and Loading Routes

In the AppModule

import { Routes, RouterModel } from '@angular/router';
const appRoutes:Routes = [
    {path: '', component: HomeComponent},
    {path: 'users', component: UsersComponent},
    {path: 'servers', component: ServersComponent}
]

In the imports, add

RouterModule.forRoot(appRoutes)

Add directive to the place where you want to render the routed views.

<div class="row">
    <router-outlet></router-outlet>
</div>

Navigating with Router Links

And in <a> tags, pass the href relative paths.
Using href reloads the page while going into the different views. This will rsset all the data the application had in the previous state ( the app state).
To avoid this problem, there is another way in which routing can be made to work:

<a routerLink="/services">Services</a>

In this manner, the page won’t reload when the link is clicked.
There is also another way, by use of PropertyBinding:

<a [routerLink]="['/users']">Users</a>

This is helpful for managing multiple level of url paths that can be passed as an array.

Understanding Navigation Paths

Appending / before a path, makes it absolute
Not appending / , will make it relative
./ is same to not adding the / at all
../ works similar to that of folder paths and moves one route back

Styling Active Router Links

The currently selected router links can be made active by using routerLinkActive attribute and setting it to the active css class.

<li role="presentation"
    routerLinkActive="active"> // active is the bootstrap css class
    <a routerLink="/">Home</a>
</li>
<li role="presentation"
    routerLinkActive="active">
    <a routerLink="/servers">Servers</a>
</li>
<li role="presentation"
    routerLinkActive="active">
    <a routerLink="/users">Users</a>
</li>

The above method will work, but one think to notice is that when the Servers or Users tab is clicked, they get selected, but the Home tab also remains selected at those times.
This is because, when the routerLinkActive is set, it checks for the availability of the router in the whole url. So when /servers or /users is clicked, it also contains the / url. So the Home also gets the active css class.
To fix this issue, there is another attribute called [routerLinkActiveOptions] which has to be set via property-binding as it requires an object.

<li role="presentation"
    routerLinkActive="active"
    [routerLinkActiveOptions]="{ exact: true }"> // active is the bootstrap css class
    <a routerLink="/">Home</a>
</li>
<li role="presentation"
    routerLinkActive="active">
    <a routerLink="/servers">Servers</a>
</li>
<li role="presentation"
    routerLinkActive="active">
    <a routerLink="/users">Users</a>
</li>

This will make sure that the css class is only applied when the link exactly matches with the route.

Navigating Programmatically

Inject the router to the constructor of whatever class it is required in.

import { Router } from '@angular/router';
export class HomeComponent {
    constructor(private router:Router) { }

    loadServers() {
        this.router.navigate(['/servers']); // It takes an array of routes to construct the final route
    }
}

Using Relative Paths in Programmatic Navigation

If in the above loadServers(), one removes the absolute url denotion / to:

this.router.navigate(['servers']);

It still wouldn’t break the app and Angular would still load “www.app.com/servers”
This is because, while calling the navigate function, Angular doesn’t know where is the route is currently at.
To know that, one needs to pass on that information which can be found in the ActivatedRoute class. So one needs to inject it into the constructor whenever it is required, and then pass on the information to the navigate() function.
So the above class would become:

import { Router, ActivatedRoute } from '@angular/router';
export class ServersComponent {
    constructor(private router:Router, private activatedRoute:ActivatedRoute) { }

    loadServers() {
        this.router.navigate(['/servers'], { relativeTo: this.activatedRoute });
    }
}

Passing Parameters to Routes

Anything passed with a colon beofe it in a route will be considered as an argument.
/users/:id – Here anything passed after the /users/ wil be considered as an id.
Multiple parameters can also be passed.

{ path: 'users/:id/:name', component: UserComponent }

Fetching Route Parameters

To get access to the parameters, in the component rendering the route, inject the ActivatedRoute in the constructor (imported from @angular/router).
And in ngOnInit, fetch the parameter from the ActivatedRoute

this.activatedRoute.snapshot.paramas["user_id"]

If multiple parameters have been passed: /users/:user_id/:user_name, these can also be parsed by using

export class UserComponent {
    constructor(private activatedRoute:ActivatedRoute) { }
    ngOnInit() {
        this.user = {
            id: this.activatedRoute.snapshot.params['id'],
            name: this.activatedRoute.snapshot.params['name']
        };
    }
}

Fetching Route Parameters Reactively

In the previous example, if the same route is called from within the view, then the view will not update with the updated data.

<a [routerLink]="['/users', 10, 'Anna']">Load Anna</a>

This link when clicked will update the url, but the data in the view won’t update. This is because, we only fetched the activatedRoute.snapshot on ngOnInit() of the view, and if we just change the data internally, the view is not getting initialized again, and hence has no idea that the data changed.
This happens because when the params get updated, Angular searches for the view to render and if it is the same as the current view, it doesn’t reinitialize it.
So as because in the previous step we only updated the user data on ngOnInit(), the data didn’t get updated on url change.
To make sure that the view works fine, one needs to subscribe to the changes happening to the route.

export class UserComponent {
    constructor(private activatedRoute:ActivatedRoute) { }
    ngOnInit() {
        this.user = {
            id: this.activatedRoute.snapshot.params['id'],
            name: this.activatedRoute.snapshot.params['name']
        };
        this.activatedRoute.params.subscribe((params:Params) => {
            this.user.id = params['id'];
            this.user.name = params['name'];
        });
    }
}

In the above line of code, params is an observable.
Observables is not a feature of Angular, but is heavily used by Angular to look forward for asynchronous tasks.
Observable is something that might happen in the future, so we subscribe to it to know if it happens and whenever it happens, we want to execute some code.

An Important Note About Route Observables

When the view is removed from view, Angular removes the view, its associated children and the event listeners in there. So one doesn’t need to do that manually.
But in case when custom observables are being created, it is required to remove everything manually in the ngOnDestroy method.
To do that, store the subscription in a variable (of type Subscription whih needs to be imported from erxjs/Subscription) and unsubscrive from that in the ngOnDestory method of the class.

import { Subscription } from 'rxjs/Subscription';
export class UserComponent implements OnInit, OnDestroy {
    ngOnInit() {
        this.paramsSubscription = this.activatedRoute.params.subscribe(
            (params:Params) => {
                this.user.id = params["user_id"];
                this.user.name = params["user_name"];
            }
        );
    }

    ngOnDestory() {
        this.paramsSubscription.unsubscribe();
    }
}

Passing Query Parameters and Fragments

In a url like http://localhost/users/1/someone?mode=edit#load
The part that follows the ? in a url (mode=edit) is the query parameter. There can be multiple of them, separated by &
The part that follows the # is the Fragment. There can only be one fragment.

<a
    [routerLink]="['/servers', 5, 'edit]"
    [queryParams]="{allowEdit: 1, sure: 'yes' }"
    fragment="loading" //[fragment]="['loading']" - Can also be passed as this
    class="list-group-item">
    {{ server.name }}
</a>

queryParams is a bindable property of the routerLink directive.
In typescript can be used as

this.router.navigate(
    ['/servers', id, 'edit'],
    {
        queryParams: { allowEdit: 1 },
        fragment: 'loadingnow'
    }
);

Retrieving Query Parameters and Fragments

Similar to the routerLinks, the query parameters as well as the fragment can be accessed from the snapshot property, or changes can be registered by subscribing to changes of the specific properties.

    console.log(this.activatedRoute.snapshot.queryParams['allowEdit']);
    console.log(this.activatedRoute.snapshot.fragment);

    this.activatedRoute.queryParams.subscribe(
      (data:Params) => {
        console.log(data['allowEdit']);
      }
    );

    this.activatedRoute.fragment.subscribe(
      (data:Params) => {
        console.log(data);
      }
    );

Practicing and Some Common Gotchas

In this case where the user_id or the server_id is a string, it is sometime possible that one might try to directly ser it somehwere expecting a number.

getServerByID(id:number) { }

If one tries to:

var id = this.activatedRoute.snapshot.params['id']
getServerByID(id) // This would fail.

That’s because the id when fetched from the url is a string by default and trying to pass a string to a function expecting a number would throw an error.
To resolved this, the string can be typecasted to a number by adding a + before it.
So,

var id = this.activatedRoute.snapshot.params['id']
getServerByID(+id) // This would work

Setting up Child (Nested) Routes

const appRoutes:Routes = [
    {path: 'servers', component: ServersComponent},
    {path: 'servers/:id', component: ServerComponent},
    {path: 'servers/:id/edit', component: EditServerComponent}
];

To nest the routes, one can do

const appRoutes:Routes = [
    {path: 'servers', component: ServersComponent, children[
        {path: ':id', component: ServerComponent},
        {path: ':id/edit', component: EditServerComponent}
    ]},
];

While this would simiplify things a bit, but would throw a runtime error.
To resolve this, and outlet needs to be provided for the children. This can be done by adding:
<router-outlet></router-outlet>
to the parent view.
TODO – See what can be done if the same router view needs to be added inside a parent router and needs to be accessed directly.

Using Query Parameters – Practice

When moving around using the navigate method, the query parameters get lost.

Configuring the Handling of Query Parameters

A new attribute needs to be added to the second parameter of the navigate function.
this.router.navigate([‘edit’], { relativeTo: this.activatedRoute, queryParamsHandling: ‘preserve’ });
queryParamsHandling – takes a string
Can be set to ‘preserve’ or ‘merge’
‘preserve’ will drop the new ones and override with the old ones.
‘merge’ will merge the old query params with any new query params being added to the currently navigate method.

Redirecting and Wildcard Routes

When we enter a route in the url abr when it is not available, the Angular throws an error.
To do that create a page-not-found component, and add it as a route to something like ‘404’ or ‘not-found’.
And in the last add a ** route and redirectTo the 404 route.

const appRoutes:Routes = [
    ...,
    { path: '404', component: PageNotFoundComponent },
    { path: '**', redirectTo: '/404' }
];

Make sure to add the ** path as the last route in the Routes array.

Important: Redirection Path Matching

A little difficult to understand/implement

Outsourcing the Route Configuration

When the routes count increases, it is better to outsource it to another file.
To do that create a new file named app-routing.module.ts

const appRoutes:Routes = [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent, children: [
          { path: ':id', component: UserComponent },
          { path: ':id/:name', component: UserComponent }
    ]},
    { path: 'servers', component: ServersComponent, children: [
          { path: ':id', component: ServerComponent },
          { path: ':id/edit', component: EditServerComponent }
    ]},
    { path: '404', component: PageNotFoundComponent },
    { path: '**', redirectTo: '/404' }
];
@NgModule({
    imports: [
        RouterModule.forRoot(appRoutes)
    ],
    exports: [
        RouterModule
    ]
})
export class AppRoutingModule { }

And this can be then added to AppModule imports []

An Introduction to Guards

When authentication is required, it would be cumbersome to replicate the check everywhere in the code.
So Angular provides a way to run some code before the route gets activated using canActivate Guard

Protecting Routes with canActivate

Create a file named auth-guard.service.ts
Make it Injectable, so that the AuthService can be injected into it.

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService:AuthService, private router:Router) {   }

  canActivate(activatedRouteSnapshot:ActivatedRouteSnapshot, routerStateSnapshot:RouterStateSnapshot):Observable | Promise | boolean {
    return this.authService.isAuthenticated().then(
      (authenticated:boolean) => {
        if (authenticated)
          return true;
        else
          this.router.navigate(['/']);
      }
    );
  }
}

Another file named auth.service.ts This fakes a real authentication module.

export class AuthService {
  loggedIn = false;
  
  isAuthenticated() {
    const promise = new Promise(
      (resolve, reject) => {
        setTimeout(() => { resolve(this.loggedIn); }, 800);
      }
    );        
    return promise;
  }
  
  logIn() { this.loggedIn = true; }
  
  logOut() { this.loggedIn = false; }
}

The canActivate function can return an Observable or a Promise or simply a ‘boolean’
So this function can work both asynchronously (Observable, Promise) or synchronously (boolean)
Add this 2 services to the providers list in AppModule.
Then, in the AppRoutes, provide as

{ path: 'servers', canActivate: [AuthGuard], component: ServersComponent, children: [
    { path: ':id', component: ServerComponent },
    { path: ':id/edit', component: EditServerComponent }
]},

The canActivate can have an array of Guards to pass through before proceeding onto the Route view.
In the above case, it will always redirect to home page on clicking Servers, because the loggedIn has been set to false.

Protecting Child (Nested) Routes with canActivateChild

The canActivateChild can be used to guard the children of a particular route.
For this the AuthGuard needs to implement the CanActivateChild interface. So the earlier AuthGuard class becomes:

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
    constructor(private authService:AuthService, private router:Router) {   }

    canActivate(activatedRouteSnapshot:ActivatedRouteSnapshot, routerStateSnapshot:RouterStateSnapshot):Observable | Promise | boolean {
        return this.authService.isAuthenticated().then(
            (authenticated:boolean) => {
                if (authenticated)
                    return true;
                else
                    this.router.navigate(['/']);
            }
        );
    }

    canActivateChild(activatedRouteSnapshot:ActivatedRouteSnapshot, routerStateSnapshot:RouterStateSnapshot):Observable | Promise | boolean {
        return this.canActivate(activatedRouteSnapshot, routerStateSnapshot);
    }
}

Now the AppRoutes can be updated as:

{ path: 'servers', canActivateChild: [AuthGuard], component: ServersComponent, children: [
    { path: ':id', component: ServerComponent },
    { path: ':id/edit', component: EditServerComponent }
]},

If the canActivate is not guarded and the canActivateChild is passed, then the /servers/ can be opened without authorization, but not the children.

Controlling Navigation with canDeactivate

As we can control if we can navigate into a router, we can also manage if someone can navigate away from a router.
This comes helpful if the user has done some changes and haven’t saved it yet.
In the edit-server folder add a new file named can-deactivate-guard.service.ts with the following code:

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

CanDeactivate is a generic type and will wrap our own interface

export class CanDeactivateGuard implements CanDeactivate {
    canDeactivate(component:ICanDeactivateComponent,
        activatedRouteSnapshot:ActivatedRouteSnapshot,
        currentStateSnapshot:RouterStateSnapshot,
        nextStateSnapshot?:RouterStateSnapshot):Observable | Promise | boolean {

        return component.canDeactivate();
    }
}

Now the component needs to also implement the ICanComponentDeactivate interface, and have the canDeactivate() which will contain the logic to allow/disallow the user from redirecting to the new Route.

export class EditServerComponent implements OnInit, ICanDeactivateComponent {
    canDeactivate():Observable | Promise | boolean {
        if (this.allowEdit) {
          if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) && !this.changesSaved) {
            return confirm("Do you want to discard the changes?");
          }
        }
        return true;
    }
}

Add the CanDeactivateRoute to the route’s parameters

{ path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] }

and also add it to the AppModule providers array.

Passing Static Data to Route

One can also pass data to the route via the data property in the route.
For example in an error page, one might need to some dynamic message that has been passed from the route.

{ path: '404', component: PageNotFoundComponent, data: { message: 'Page Not Found!'} },

And in the component, the data can be fetched by accessing it from the ActivatedRoute.

export class PageNotFoundComponent implements OnInit {
  // The errorMessage can be string-interpolated to the html view.
  errorMessage:string;

  constructor(private activatedRoute:ActivatedRoute) { }

  ngOnInit() {
    this.errorMessage = this.activatedRoute.snapshot.data['message'];
    // One can also subscribe to changes in the data.
    this.activatedRoute.data.subscribe((data:Data) => {
      this.errorMessage = data['message'];
    });
  }
}

Resolving Dynamic Data with the Resolve Guard

Sometimes it is required to wait for the data to load and then only update the screen based on the data.
server-resolver.service.ts

interface Server {
    id:number;
    name:string;
    status:string;
}
@Injectable()
export class ServerResolver implements Resolve {
    constructor(private serversService:ServersService) {}
    resolve(activatedRouteSnapshot:ActivatedRouteSnapshot, routerStateSnapshot:RouterStateSnapshot):Observable | Promise | Server {

        return this.serversService.getServer(+activatedRouteSnapshot.params['id']);
    }
}

Add this ServerResolver to the AppModule in the providers array.
In the AppRouting module, add the resolve parameter. Anything passed in the key can be accessed from the activatedRoute.data

{ path: ':id', component: ServerComponent, resolve: { server: ServerResolver} },

Now the server.component.ts can be updated for:

ngOnInit() {    
    this.activatedRoute.data.subscribe((data:Data) => {
        this.server = data['server'];
    });
}

Understanding Location Strategies

The urls are always handled by the server first. So mostly when we hit a url in a real server
www.xyz.com/servers, the server will look for the route in the server side and would try to return a 404 page.
To bypass this, one needs to make sure that the 404 redirects to the index.html file such that Angular can take care of it from then on.
But there are also some browsers where this wouldn’t work. In those cases, there is a second approach where a # would be appended after the root url.
RouterModule.forRoot(appRoutes, { useHash: true })
This would return something like
www.xyz.com/#/servers
Using this, the browser would igniore anything after the #, and the rest can be handled by Angular.
But this is not a pretty way and one should try to avoid this manner.

Note: Should use routerLink instead of href to make sure that the page dopesn’t reload and the redirection is handled by Angular

In the case of how the routes should follow each other, a declaration of

{ path: ':id', component: RecipeDetailComponent },
{ path: 'new', component: RecipeEditComponent },

would create problems as if you pass recipes/new in the url, it would try to parse the new route as an id and would throw an error, because it wouldn’t be able to find an item with new index.
So the new url route should come before the :id
It should be made sure that angular understands to take the url as a hardcoded route or a dynamic variable

Authentication & Route Protection in Angular

While most of the authentication logic is setup in the backend,w e only need to know how to use the authentication system in the frontend.
As we have already used firebase and it has a pretty easy setup for authentication system in the backend, we are going to use that.

How Authentication Works in Single Page Applications

In a traditional web app,normally all the view generation is done in the server and the frontend receives the fully built html and just displays it.
But in the case of SPA, thes erver only sends one html file, and Angular is responsible for dynamically changing the content.
In traditional web apps, when a user logs in, the credentials are sent tot he server, who in turn validates the data, and if valid, creates a session. Then it returns the session cookie to the client who for every request sends back the session cokie to the sever for validation for any further requests.
In the case of SPA’s, when the user sends the validation credentials, the server sends back a hashed token that has been generated with soem keys. So for every request from then on, the frontend will send back the token with the request so that the server can validate the information.
Traditional – Server creates a session and remebers the client as the frontend and backend are tightly coupled.
SPA – Server doesn’t create any session and uses the token to recognise the user. Frontend and backend are loosely coupled.

More About JWT

Want to learn about the Token which is exchanged?
The following page should be helpful: https://jwt.io/ – specifically, the introduction: https://jwt.io/introduction/

Signing Users Up

npm install firebase --save

Firebase need to be initialized at app startup. So add the configuration in the app.component

firebase.initializeApp({
    apiKey: '....',
    authDomain: '...'
});

There are others whuich can be found in the firebase console, btu these two are enough to get started.
For signup, can be used for email/password combo as

firebase.auth().createUserWithEmailAndPassword(email, password)
    .catch(error => console.log(error));

Signin Users In

To signin,

firebase.auth().signInWithEmailAndPassword(email, password)
        .then(response => console.log(response))
        .catch(error => console.log(error));

The response contains a lot of details about the authorization and also contains the userid and the access tokens and it is stored in the localStorage by firebase also.

Requiring a Token (on the Backend)

In firebase to limit the authenticated users to read/write to the database can be managed by updating the rules for the database.

{
    "rules": {
        ".read": "auth != null",
        ".write": "auth != null"
    }
}

Now the requests to firebase will require the token to do any operations.

Sending the Token

To send the token, in the AuthService set the token that is fetched by

firebase.auth().currentUser.getToken() // this will return a promise
.then(reponse => {
    this.token = reponse;
});

Now this token needs to be sent in every request by appending the token at the end of the request url

this.http.get/put('...some_url.../items.json?token=' + this.token);

Checking and Using Authentication Status

To check the authentication status, one can check for the token to make sure that the authentication information is available for performing tasks. In the AuthService

isAuthenticated() {
    return this.token != null;
}

Now this can be used in the HTML to show/hide items.

Adding a Logout Button

In the AuthServicem add

logout() {
    this.auth().signOut();
    this.token = null;
}

Route Protection and Redirection Example

On the signIn function in AuthService, we can use the router to redirect the user to the page we want to.
Create an AuthGuard

export class AuthGuard implements CanActivate {
  constructor(authService: AuthService) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.authService.isAuthenticated();
  }
}

And now this AuthGuard can be used in the routing module.

{ path: 'recipes/new', component, 'RecipeEditComponent', canActivate: [AuthGuard] }

Possible Improvements

One can of course improve this app even more. Some ideas:
Check if a token is present at application startup (check the localStorage manually or use the Firebase SDK to do so – just make sure that you somehow wait for the SDK to finish its initialization)
Redirect the user if he want to access a protected route (right now, nothing happens) – inject the router and call this.router.navigate(…) to do so
Redirect the user on logout so that he’s not able to stay on pages which are reserved for authenticated users – you can simply inject the router and call this.router.navigate(…) in the logout() method

Leave a Reply

Your email address will not be published. Required fields are marked *