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 😊.