Using Angular Custom Decorators in an Ionic application (Part 1)
Angular 2 brought forward a TypeScript mechanism: the Decorators.
In the past, we used special methods from the AngularJS library to specialise our code.
The Angular 2 mechanism is more simple and flexible, we just import a Decorator and put it at the right place.
Angular Decorators are quite similar to React's HOC (that we have seen in this tutorial), we have one original Component and we can add some complex features to this Component by just adding two lines of code.
In this course, we will create our own Angular Decorators in an Ionic application. We will incrementally increase the complexity by going through:
- Property Decorators
- Method Decorators
- Class Decorators
As usual, we create an Ionic application:
ionic start ionic-decorators tabs --type=angular
We are creating a Tabs project because we will need the Ionic LifeCycle hooks for the last case.
Furthermore, the Ionic stack is perfectly configured for the use of Angular Decorators.
Property Decorators
We will start with a simple property modification tracker.
Our aim is to track the changes of an Angular Component's property without implementing anything complex in this Component.
We will use the first tab of our Ionic Angular application:
import { Component } from '@angular/core';
import { PropsChangedTracker} from '../decorators/propsChangedTracker.decorator';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
@PropsChangedTracker() // PropertyDecorator
simpleProp;
constructor() {
this.simpleProp = 1;
}
}
We import a custom PropsChangedTracker located in a newly created decorators directory.
This is a PropertyDecorator, so we use it on a simpleProp property and we finally initialize this property in the constructor.
The PropsChangedTracker Angular Decorator is as follow:
export function PropsChangedTracker() {
return function (target, key) {
let value;
const onPropsSet = (newValue) => {
console.log("The property:", key, "changed, its new value is:", newValue);
value = newValue;
}
Object.defineProperty(target, key, {
set: onPropsSet,
get: () => value
});
}
}
We are exporting a module named PropsChangedTracker, this module returns a function.
In this case, we are using the target and the key parameters.
Those parameters vary depending on the Decorator's type.
The Object.defineProperty method is used to modify the behavior of our target.
We specify that for a specific key, a specific setter and getter method must be called.
We grab the newValue that should be used and pass it to a onPropsChanged method. This value will then be used to set a value variable that will be returned when the getter will be used.
When the Tab1's construtor is done, the modification of simpleProps should be logged:
It's quite a simple case, here is another more business related.
We will create a Traductor Decorator that will be working exclusively with the property's name, like this:
export function Traductor(selectedLanguage) {
const traductions = {
eng: {
welcomeMessage: 'Welcome'
},
fr: {
welcomeMessage: 'Bienvenue'
}
};
return function (target, key) {
Object.defineProperty(target, key, {
writable: false,
value: traductions[selectedLanguage][key]
});
}
}
The Traductor Decorator will receive a selectedLanguage parameter.
We start by initializing a traductions Object. In the real world, this should be done in a service that would bring back a JSON file. Our object is quite simple, there is one part for the English language and another one for the French one.
We only need to prevent the modification of the property by setting the writable property to false and initialize the value property to our dictionary's value.
Last steps: using our new Angular Custom Decorator in the Tab2 page:
import { Component } from '@angular/core';
import { TimeTracker } from '../decorators/timeTracker.decorator';
import { Traductor } from '../decorators/traductor.decorator';
@Component({
selector: 'app-tab2',
templateUrl: 'tab2.page.html',
styleUrls: ['tab2.page.scss']
})
@TimeTracker("Tab2")
export class Tab2Page {
@Traductor("eng") // PropertyDecorator
welcomeMessage: String;
}
We import the Traductor Decorator and tell it that we want the English translation.
Then we display the value in the <ion-toolbar>:
<ion-header>
<ion-toolbar>
<ion-title text-center>
{{welcomeMessage}}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>
The result:
Method Decorators
Now let's create a Method Decorator that will hijack a Component's method.
We will name it MethodHijacker:
export function MethodHijacker() {
return function (target, propertyKey, descriptor) {
descriptor.value = function() {
console.log("All your base are belong to us")
}
return descriptor;
}
}
This time we use the descriptor and modify its value with our own method, then we return the modified descriptor.
We now move to the last Tab to use it:
import { Component } from '@angular/core';
import { MethodHijacker } from '../decorators/methodHijacker.decorator';
@Component({
selector: 'app-tab3',
templateUrl: 'tab3.page.html',
styleUrls: ['tab3.page.scss']
})
export class Tab3Page {
constructor() {
this.sayGoodBye();
}
@MethodHijacker() // MethodDecorator
sayGoodBye() {
console.log("Good bye");
}
}
The MethodHijacker Decorator is imported and hijack a simple sayGoodBye method.
When we call this method in the constructor, we have:
Class Decorators
We are now at the last Decorator for this lesson: the Class Decorator.
We are going to create an Angular Class Decorator that will show how much time a user spent on an Ionic View.
Ionic Views go trough a specific life cycle, from their creation to their destruction, here is a tutorial on the whole lifecycle.
In our case we will only use ionViewDidEnter and ionViewWillLeave hooks to create the following TimeTracker Decorator:
export function TimeTracker(viewName) {
return function (constructor) {
var startTime;
const ionViewDidEnterHook = "ionViewDidEnter";
const ionViewWillLeaveHook = "ionViewWillLeave";
const original = constructor.prototype[ionViewDidEnterHook];
constructor.prototype[ionViewDidEnterHook] = function ( ...args ) {
startTime = new Date();
original && original.apply(this, args);
}
const original2 = constructor.prototype[ionViewWillLeaveHook];
constructor.prototype[ionViewWillLeaveHook] = function ( ...args ) {
const endTime = new Date();
const timeSpent = endTime.getTime() - startTime.getTime();
console.log("The user spent", timeSpent, " ms on:", viewName);
original2 && original2.apply(this, args);
}
}
}
The TimeTracker Decorator Module receives the name of the current view as a viewName parameter.
The Class Decorator receives as a first parameter a constructor. Always keep in mind that the Module and the Decorator arguments are two different things. The first one is initialized by us when we call the Decorator, the other one depends on the Decorator's type.
We will grab the original ionViewDidEnter and ionViewWillLeave methods from this constructor to keep the user's predefined mechanism.
We will override those methods, first the ionViewDidEnterHook one:
constructor.prototype[ionViewDidEnterHook] = function ( ...args ) {
startTime = new Date();
original && original.apply(this, args);
}
We initialize a startTime variable with the current Date then we call the original ionViewDidEnter method with the original context and arguments.
On the other side, when the user leaves the view:
constructor.prototype[ionViewWillLeaveHook] = function ( ...args ) {
const endTime = new Date();
const timeSpent = endTime.getTime() - startTime.getTime();
console.log("The user spent", timeSpent, " ms on:", viewName);
original2 && original2.apply(this, args);
}
We use the startTime to display how long a user spent on the view (in milliseconds).
Like before we reuse the original ionViewWillLeave method so the developers don't wonder why their code is not running.
We only need to add the TimeTracker Decorator to each Ionic Tabs Components to start tracking the time:
import { Component } from '@angular/core';
import { TimeTracker } from '../decorators/timeTracker.decorator';
@Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss']
})
@TimeTracker("Tab1")
export class Tab1Page {
}
If everything is going good, we should have the following result:
Conclusion
We have seen three types of Angular Decorators:
- The Property Decorators
- The Method Decorators
- The Class Decorators
Decorators are really great for adding new complex features to Components.
They can take a Component, decorate it with an augmented behavior and seamlessly call the original one.
In the next tutorial, we will see how to use the Ionic Modal API in a Decorator and combine it with the TimeTracker one.