Create a TypeScript custom decorator
What is a decorator?
Just a JavaScript function!
For more details, and according to the TypeScript documentation,
Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript.
Decorators provide a way to extend methods, classes, properties and parameters with custom logic.
If you have already worked with Angular, you are probably familiar with those @Input() and @ViewChild() annotations, which are in fact TypeScript decorators!
To enable them (as they are experimental), you can set experimentalDecorators: true in your tsconfig.json file.
What does it look like?
To a simple JavaScript function 😄:
function logger(target) {
// do something with 'target' ...
}
We can also write it as a "Factory".
A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime, and allows it to pass arguments to the decorator.
function logger(type: string) {
// this is the decorator factory, it sets up
// the returned decorator functionreturn function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}
Example: Confirmation dialogs
In web applications, we often (always?) need to have confirmation dialogs to prevent users from performing important actions by mistake (delete, cancel...).
And we often end up with a lot of methods looking like this:
onDelete(ids: number[]): void {
this.confirmationDialogService.open(
{
message: 'You are about to delete item(s). Continue?',
accept: () => this.deleteItems(ids)
}
);
}
It would be nice to be able to only write something like:
@Confirmable // Custom decorator here
onDelete(ids: number[]): void {
this.deleteItems(ids); // only declare the method to apply if user confirms
}
Well, that's what we are going to achieve here 🚀!
Implementation
First let's create a file confirmable.decorator.ts.
Inside, we need to export a function which we'll call Confirmable.
export function Confirmable (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
// cache the original method for later useconst originalMethod = descriptor.value;
// we write a new implementation for the method
descriptor.value = async function (...args) {
const message = 'Are you sure that you want to perform this action?';
// ConfirmationDialogService must be injected to be used.// AppModule needs to be configured with injector, more on that later (*).
const confirmationDialogService = AppModule.injector.get<ConfirmationDialogService>(
ConfirmationDialogService
);
const res: boolean = await confirmationDialogService.open(message);
if (res){ // if user clicked yes,// we run the original method with the original argumentsconst result = originalMethod.apply(this, args);
// and return the result
return result;
}
};
return descriptor;
};
Now you can try to use it: it already works.
@Confirmable
onDelete(ids: number[]): void {
this.deleteItems(ids);
}
(*) : in order to inject the confirmationDialogService into our new decorator, you will have to write this in your AppModule (or the module where the service is provided):
export class AppModule {
static injector: Injector;
constructor(injector: Injector) {
AppModule.injector = injector;
}
}
Custom confirmation message as parameter
But what if we want to define custom confirmation messages to display to the user?
We would need to twist a bit our new decorator transforming it to a factory!
const DEFAULT_MESSAGE = 'Are you sure that you want to perform this action?';
export function Confirmable(message: string = DEFAULT_MESSAGE) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
const confirmationDialogService = AppModule.injector.get<ConfirmationDialogService>(
ConfirmationDialogService
);
const res = await confirmationDialogService.open(message);
if (res) {
const result = originalMethod.apply(this, args);
return result;
}
};
return descriptor;
};
}
Here we go! Now we can define a custom message while using the decorator:
@Confirmable('You are going to delete item(s). Continue?')
onDelete(ids: number[]): void {
this.deleteItems(ids);
}
And that's it for today, hope you learned something useful 😊.