Decorator
Decorators are used for meta programming to add extra configuration or extra logic.
Angular is heavily depended on Decorator.
Basic
function logger(constructor: Function) {
console.log(constructor);
}
@logger
class Test {
constructor(
public name: string
) {}
}
logger()
is executed when Test
is definded, not is instantiated.
Decorator Factory
function Logger(message: string) {
return function(constructor: Function) {
console.log(message);
console.log(constructor);
}
}
@Logger('Logging Person')
class Test {
constructor(
public name: string
) {}
}
Order of decorator
function Logger(message: string) {
return function(constructor: Function) {
console.log(message);
}
}
@Logger('Logging Person1')
@Logger('Logging Person2')
class Test {
constructor(
public name: string
) {}
}
// Output
// Logging Person2
// Logging Person1
The first Decorate Factory executes first, then put its returned function into a STACK. The second does the same.
That's why the output is printed from bottom to up.
Return a class in Decorator
To let the logic in Derator runs while the class is instantiated, we can return the class (technically, the constructor function) in Decorator.
function WithTemplate(template: string, hookId: string) {
console.log('TEMPLATE FACTORY');
return function<T extends { new (...args: any[]): {name: string} }>(
originalConstructor: T
) {
return class extends originalConstructor {
constructor(..._: any[]) {
super();
console.log('Rendering template');
const hookEl = document.getElementById(hookId);
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = this.name;
}
}
};
};
}
@WithTemplate('<h1>My Person Object</h1>', 'app')
class Test {
constructor(
public name: string
) {}
}
Autobind
class Printer {
message = "It works.";
showMessage() {
console.log(this.message);
}
}
const p = new Printer();
const button = document.querySelector('button');
button.addEventListener('click', p.showMessage); // output after clicking: undefined.
To fix this problem, we need bind the object p
to the p.showMessage
.
button.addEventListener('click', p.showMessage.bind(p));
Or, we can create a decorator to bind p
.
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
const boundFn = originalMethod.bind(this);
return boundFn;
}
};
return adjDescriptor;
}
class Printer {
message = "It works.";
@Autobind
showMessage() {
console.log(this.message);
}
}
Validation
interface ValidatorConfig {
[property: string]: {
[validatableProp: string]: string[]; // ['required', 'positive']
};
}
const registeredValidators: ValidatorConfig = {};
function Required(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [...registeredValidators[target.constructor.name][propName], 'required']
};
}
function PositiveNumber(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [...registeredValidators[target.constructor.name][propName], 'positive']
};
}
function validate(obj: any) {
const objValidatorConfig = registeredValidators[obj.constructor.name];
if (!objValidatorConfig) {
return true;
}
let isValid = true;
for (const prop in objValidatorConfig) {
for (const validator of objValidatorConfig[prop]) {
switch (validator) {
case 'required':
isValid = isValid && !!obj[prop];
break;
case 'positive':
isValid = isValid && obj[prop] > 0;
break;
}
}
}
return isValid;
}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t: string, p: number) {
this.title = t;
this.price = p;
}
}