Creating Your Own Javascript Decorators in Aurelia

Decorators are currently a stage 2 proposal in Javascript and they allow you to decorate classes, class properties and methods. If you have worked with Aurelia for longer than 5 minutes or other frameworks such as Angular, you will already be familiar with them.

At the core of a decorator, it is a function that returns a function. It wraps the context of wherever it is applied. Decorators allow you to add new properties/methods to a class or change existing behaviours.

Aurelia already utilises decorators quite extensively, but it does not rely on them for everyday use. The existence of decorators allows you to mark your applications up using convenient shorthand.

It is important to remember that decorators are a Javascript language construct and not specific to any framework like Aurelia. Given Aurelia leverages ES2015 classes, we can create and use our decorators without changing anything.

Decorator Types

When we talk about decorators, there are three categories. A class decorator (decorates a class), a class field decorator (inline properties on classes) or a method decorator (a function, either standalone or within a class). You can even create a decorator that combines all three types into one. For the context of this article, we will only be focusing on class decorators.

Creating a class decorator

If you have used Aurelia a little bit, decorators such as; @inject, @singleton and @customElement might be familiar to you already. These are examples of a class decorator.

The inject decorator is used in the following manner:

import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';

@inject(Router)
export class MyClass {
    ...
}

If you look at the actual implementation for the inject decorator in the Aurelia codebase here you will notice that a class property called inject is being assigned on the class here.

Because decorators in Aurelia are optional and nothing more than convenient shortcuts, you can write the above like this:

import {Router} from 'aurelia-router';

export class MyClass {
    static inject = [Router];
}

Let’s create a simple decorator that adds a property to our class called isAwesome a boolean property which will get set on the class itself.

@isAwesome(true)
export class MyClass {

}

function isAwesome(val) {
    return function(target) {
        target.isAwesome = val;
    }
}

In the context of this decorator, target is the class we are using the decorator on and gives us access to the class itself, including the prototype object.

Creating an Aurelia decorator

The above decorator is quite simple. It adds a property to our class and does not do anything exciting. Now, we are going to be creating a decorator which shorthands the Aurelia router lifecycle method canActivate.

function canActivate(val, redirectTo = '') {
    return function(target) {
        target.prototype.canActivate = () => {

            if (!val) {
                if (redirectTo === '') {
                    window.location.href = '/404';
                } else {
                    window.location.href = redirectTo;
                }
            }

            return val;
        };
    };
}

While this is a very rough decorator that would need some improvement before serving a genuine purpose, it showcases how you can modify a class, even adding new methods to the prototype with ease.

Now, let’s clean it up and make it more useful and delightful to look at using some nice new Javascript syntax.

const canActivate = (resolve) => {
    return (target) => {
        target.prototype.canActivate = () => {
            return resolve();
        };
    };
};

Now, we have to pass through a function which allows us to be more flexible in what we can pass through. Still, it could be a lot more useful. What if we wanted to access the current route or get passed in parameters like we can with a class defined method?

const canActivate = (resolve) => {
    return (target) => {
        target.prototype.canActivate = (params, routeConfig, navigationInstruction) => {
            return resolve(params, routeConfig, navigationInstruction);
        };
    };
};

Okay, now we have a decorator which accepts a callback function and has access to the route parameters, the routeConfig and current navigation instruction.

To use our new fancy decorator, we pass through a function which needs to return a value (boolean, redirect):

@canActivate((params, routeConfig, navigationInstruction) => {
    return !(routeConfig.auth);
})
export class MyClass {

}

Apply this decorator will deny the class getting activated if the route has an auth property of true. We inverse the boolean check when we return it.

Just when you thought we couldn’t improve our decorator anymore, you might have noticed there is a flaw in what we have created. It always assumes that our callback returns something. If it doesn’t, we’ll stop the view-model from executing.

Let’s tweak our decorator slightly:

const canActivate = (resolve) => {
    return (target) => {
        target.prototype.canActivate = (params, routeConfig, navigationInstruction) => {
            let resolveCall = resolve(params, routeConfig, navigationInstruction);

            return typeof resolveCall !== 'undefined' ? resolveCall : true;
        };
    };
};

We still require a callback function, but if the developer doesn’t return anything from their callback function, we’ll return true as to ensure the view-model executes fine.

If you wanted to redirect all visitors to a different route if it’s an authenticated route, you could do something like this:

import {Redirect} from 'aurelia-router';

@canActivate((params, routeConfig, navigationInstruction) => {
    return (routeConfig.auth) ? new Redirect('/robots') : true;
})
export class MyViewModel {

}

You would want to add in some additional logic to check if a user is logged in and then redirect accordingly, our example does not accommodate for this. We have just showcased how easy it is to write a decorator which can leverage existing methods and properties, as well as defining new ones.

Creating a decorator that sets the title based on the current route

When it comes to setting the title of the page based on the current route, it can be confusing for newcomers. So, using what we have learned, we are going to create a decorator which allows us to set the title from within a view-model in an Aurelia application.

We want to create a class decorator which hooks into the activate method and accesses the routeConfig, which contains the current navigation model and a method for setting the title.

Our decorator should support passing in a function which returns a string value or the ability to pass in a string directly.

const setTitle = (callbackOrString) => {
    return (target) => {
        ((originalMethod) => {
            target.prototype.activate = (params, routeConfig, navigationInstruction) => {
                let newtitle = typeof callbackOrString === 'function' ? callbackOrString(params, routeConfig, navigationInstruction) : callbackOrString;
                
                if (newtitle !== null && newtitle) {
                    routeConfig.navModel.setTitle(newtitle);
                }

                if (originalMethod !== null) {
                    originalMethod(params, routeConfig, navigationInstruction);
                }
            };
        })(target.prototype.activate || null);
    };
};

One important thing to note is our callback function (if one is passed in) gets the same arguments that the activate method does. This means we get parameters, the current route configuration and navigation instruction.

Using it

Assuming you have imported the decorator above or it exists in the same file as your view-model, let’s show how this decorator is used.

Callback function

We pass in a function which returns a string. This scenario might be helpful if you want to determine if the current user is logged in and display their name in the title.

@setTitle((params, routeConfig, navigationInstruction) => { return 'My Title'; }) 
export class MyViewModel {

}

String value

Passing in a string value will just set the page to this string without the use of callback functions. Useful for pages which don’t need dynamic titles.

@setTitle('My Title') 
export class MyViewModel {

}

Conclusion

In this article, we didn’t even cover the full scope of what you can do with decorators. We have only just scraped the surface. In future articles, we might explore other types of Javascript decorators. All you need to remember is decorators are functions which “decorate” whatever they’re being used with.

As a further exercise, maybe think of other things you can write decorators for, to save you some time.

Further reading

Leave a Reply

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