Frontend JavaScript TypeScript

Exploring Aurelia’s route pipelines

When coding web applications we sometimes come to the point when we need to perform some action during user’s navigation. One of the most common examples would be authorization and checking whether the user has appropriate roles. Fortunately, Aurelia provides an access to route pipeline so it’s super easy to add some extra steps 🙂 Let’s get to work!

 

Inspecting RouterConfiguration

In order to add our custom pipeline steps, we need to create some routing for the application. I won’t describe the whole process since it’s not directly related to the today’s topic but here’s some basic implementation got from the Aurelia Hub:

 


import {RouterConfiguration, Router} from 'aurelia-router';

export class App
{
    router: Router;

    configureRouter(config: RouterConfiguration, router: Router)
    {
        this.router = router;    

        config.map([
            {route: '', moduleId: ''}
        ]);
    }
}

 

As we can see one of the parameters used in the configure method is the config object (which is an instance of the RouterConfiguration class).
Let’s check its available methods:

 

sample1

 

As marked there are a couple of methods which might help us with creating custom pipeline steps. Therefore I’d like to discuss them in order from the earliest called:

 

  • addAuthorizeStep – adds a authorize step to the pipeline. The step is called between loading route’s step and calling the view-model’s canActivate method (if defined)
  • addPreActivateStep – adds preActivate step to the pipeline. This step is called between view-model’s canActivate method and the previous view-model’s deactivate method (if defined).
  • addPreRenderStep – adds preRender step to the pipeline. The step is called between view-model’s activate method but before the view is rendered.
  • addPostRenderStep – adds postRender step to the pipeline. The step is called after the view is rendered.

 

In addition to the above-mentioned methods, there is the fifth one called addPipelineStep. This one is a universal way to create chosen step which in addition to the step object requires also its name (which is just a string).

 

The implementation

Knowing the whole pipeline we can play with that a little. Let’s add all four steps to the application’s route. The implementation is given below:

 

import {RouterConfiguration, Router, NavigationInstruction, Next} from 'aurelia-router';

export class App {
    router: Router;

    configureRouter(config: RouterConfiguration, router: Router) {
        this.router = router;

        config.addAuthorizeStep(AuthorizeStep); //alternate way: config.addPipelineStep('authorize', AuthorizeStep);       
        config.addPreActivateStep(PreActivateStep); //alternate way: config.addPipelineStep('preActivate', AuthorizeStep); 
        config.addPreRenderStep(PreRenderStep); //alternate way: config.addPipelineStep('preRender', AuthorizeStep);   
        config.addPostRenderStep(PostRenderStep); //alternate way: config.addPipelineStep('postRender', AuthorizeStep);   

        config.map([
            { route: '', moduleId: 'books', }
        ]);
    }
}

export class AuthorizeStep {
    run(navigationInstruction: NavigationInstruction, next: Next): Promise<any> {
        console.log("I'm inside the authorize step!")
        return next();
    }
}

export class PreActivateStep {
    run(navigationInstruction: NavigationInstruction, next: Next): Promise<any> {
        console.log("I'm inside the pre activate step!")
        return next();
    }
}

export class PreRenderStep {
    run(navigationInstruction: NavigationInstruction, next: Next): Promise<any> {
        console.log("I'm inside the pre render step!")
        return next();
    }
}

export class PostRenderStep {
    run(navigationInstruction: NavigationInstruction, next: Next): Promise<any> {
        console.log("I'm inside the post render step!")
        return next();
    }
}

 

As you probably noticed, we didn’t need to implement any special interface inside each step class. All we have to deliver to aurelia is a run method which accepts two parameters of type NavigationInstruction and Next (got from the aurelia-router module). Inside the configureRouter method we registered all created steps using dedicated Add… methods, but I also presented to you the alternate ways of doing that. All right then, let’s run the app and check whether it’ working:

 

sample2

 

Okay, everything works but that was super easy, wasn’t it? How about something more useful? As I mentioned at the beginning of the article one of the most common scenario for that kind of functionalities would be a user’s authorization. Below is some code that can deal with such a requirement:

 

import {RouterConfiguration, Router,NavigationInstruction, Next, Redirect} from 'aurelia-router';

export class App
{
    router: Router;

    configureRouter(config: RouterConfiguration, router: Router)
    {
        this.router = router;

        config.addAuthorizeStep(AuthorizeStep);        

        config.map([
            {route: '', moduleId: 'books', settings: {roles: ['admin', 'superUser']}},
            {route: 'search', moduleId: 'search'}
        ]);
    }
}

export class AuthorizeStep
{
     run(navigationInstruction: NavigationInstruction, next: Next): Promise<any> 
     {  
         let user = {role: 'admin'};
         let requiredRoles = navigationInstruction.getAllInstructions().map(i => i.config.settings.roles)[0];

         let isUserPermited = requiredRoles? requiredRoles.some(r => r === user.role) : true;

         if(isUserPermited)  
            return next();

         return next.cancel();
     }
}

 

Let’s start with the route declaration. As you can see I added a parameter to the default module called settings. This allows putting some additional information on each route. In this case, I put the array with roles names, so if the user wants to redirect to the view, he needs to be in one of the defined roles. Now, moving forward to the run implementation, first, we have to get the user object. Normally I’d inject some AuthService or anything else that keeps the user’s object but that’s not the point of this example 😉 Having a user we need to get roles defined for the specific view. For this purpose, we can use the navigationInstruction object which gives us an access to the route’s settings. After that, all we need to do is checking whether the user is in one of the defined roles. If the condition is satisfied, we invoke next() to exit the authorization step and move forward in a pipeline. Otherwise, we reject the operation by invoking cancel method. It worthwhile to mention that this method has also an overload allowing us to redirect user to the specific view, like:

 

 return next.cancel(new Redirect('nameOfTheRoute')); 

 

Summary

That’s all I prepared for you today! I hope that this article will help you in the future while creating your awesome Aurelia apps! As I wrote earlier there are many scenarios that can be easily handled by the route pipelines (not only authorizing). The above example was just tip of the iceberg. As always, I encourage you to follow me on Twitter and Facebook just to be up to date with my new articles!