Getting started with Angular decorators

What are Angular Decorators

Angular decorators are functions we applied on classes, parameters, properties and methods in Angular. If you have already built a full Angular application you have certainly use decorators. Actually, decorators are like middleware. It means they are executed before/after processing the entity they are attached on.

Class decorators

Class decorators are intensively used in Angular. They allow us to know the particularity of the class. In that way, we know if the entity is a service, a component or a module. Thus, depending on the decorator, the class behavior is completely different.

import { Injectable } from '@angular/core';

@Injectable()
export class Service {

  constructor() {}

}

Parameter Decorators

Parameter decorators allows you to use decorators directly on a parameter. The next example shows you a real use case of parameter Decorators:

import {Component, Inject, PLATFORM_ID} from '@angular/core';

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.scss']
})
export class ExampleComponent {

  constructor(@Inject(PLATFORM_ID) public platformId) {
  }

}

The @Inject(PLATFORM_ID) allows you to have access to the environnement Angular is running on (server or browser). This is frequently used when you are building an Angular universal application. (Note that, when we use service provider in Angular, we don’t need to use this @Inject() decorator in constructor parameters because Angular does it for us when we declare in module that the service is a provider or when we use the class decorator @Injectable({ providedIn: ‘root’ }) on the service class)

Property Decorators

Property decorators allow us to give a certain ability to a class property. Then, when Angular instantiates a new class, it checks and runs all property decorators.

import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {

  @ViewChild('element', {static: false})
  element: ElementRef;

  ngAfterViewInit(): void {
  }

}

In this example, the element property will be an ElementRef Object from the DOM.

Method decorators

Method decorators allow you to use a middleware on a function. This kind of decorators are quite interesting because they can also trigger the function they are attached on from a particular event.

import {Component, HostListener} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  @HostListener('window:scroll', ['$event'])
  onWindowScroll(event: Event) {
  }

}

In this example, the onWindowScroll(event: Event) function will be called each time the decorator is triggered, and more interesting, we can retrieve the event parameter from this decorator. Now, we know the different type of decorators, let’s see how to create our own custom decorators.

Custom Angular decorators

Custom class decorators

Actually, class decorators are simple functions we can create by doing this:

export function Log(target) {
  console.log('Here is the information of the decorated entity:', target);
}

log.decorator.ts

And then you can call it by doing this:

import {Component} from '@angular/core';
import {Log} from './log.decorator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
@Log
export class AppComponent {

  constructor() {
    console.log('We are in the constructor');
  }

}

app.component.ts

And the result will be:

Here is the information of the decorated entity: class AppComponent {
    constructor() {
        console.log('We are in the constructor');
    }
}
We are in the constructor

This example shows you two things. The first one, decorators are middleware so they are executed before instantiating the class. The second one, we can easily retrieve information from the decorated entity. There is something else quite important to understand about this example. When we use @Log, we consider the type of the function Log, and here it’s () => void. That’s why we cannot use parenthesis on the @Log decorator, it’s because the return type is void. But we can fix this by doing this:

export function Log() {
  return (target) => {
    console.log('Here is the information of the decorated entity:', target);
  };
}

log.decorator.ts

import {Component} from '@angular/core';
import {Log} from './log.decorator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
@Log()
export class AppComponent {

  constructor() {
    console.log('We are in the constructor');
  }

}

app.component.ts

This preview example means now we can pass data to the decorators like this:

import {Component} from '@angular/core';
import {Log} from './log.decorator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
@Log({
  type: 'async',
  frequency: 'daily'
})
export class AppComponent {

  constructor() {
    console.log('We are in the constructor');
  }

}

app.component.ts

export function Log(configuration: { type: 'sync'| 'async', frequency: 'daily' | 'weekly' | 'monthly'}) {
  return (target) => {
    console.log('Here is the information of the decorated entity:', target);
    console.log('configuration:', configuration);
  };
}

And the result will be:

Here is the information of the decorated entity: class AppComponent {
    constructor() {
        console.log('We are in the constructor');
    }
}
configuration: {type: "async", frequency: "daily"}
We are in the constructor

If you do understand everything I just explained, you should grasp all Angular Class decorators.

Custom method decorators

Here is the way you create a custom method decorator:

export function LogResult() {
  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const original = descriptor.value;
    descriptor.value = ( ... args: any[]) =>{
      const result = original.apply(this, args);
      console.log(result);
    };

    return descriptor;
  };
}

log.decorator.ts

And then you can use it like this:

import {Component, OnInit} from '@angular/core';
import {LogResult} from './operation.decorator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{

  constructor() {
  }

  ngOnInit(): void {
    this.add(1, 5);
    this.add(1, 10);
    this.add(3, 35);
    this.add(20, 50);
  }

  @LogResult()
  add(a: number, b: number) {
    return a + b;
  }

}

In this example, each time we call the function add, we execute the @LogResult(). This decorator returns a function with three parameters:

  • Target (Returns all functions in the current class)
  • PropertyKey(Returns the name of the targeted function)
  • Descriptor(All information about the targeted method)

The last parameter allows you to run code before and/or after the targeted method. In this example, we run code only after the execution of the add method.

Custom property decorators

In the next example, we want to create a constant decorator, meaning no matter the value set to the property, the value will stay the same.

import {Component, OnInit} from '@angular/core';


function propertyMapper<T>(prototype: any, key: string, mapper: () => T) {
  const values = new Map();
  Object.defineProperty(prototype, key, {
    set(firstValue: any) {
      Object.defineProperty(this, key, {
        get() {
          return values.get(this);
        },
        set() {
          values.set(this, mapper());
        },
        enumerable: true,
      });
      this[key] = firstValue;
    },
    get: () => {
    return mapper();
  },
  enumerable: true,
    configurable: true,
});
}

function Constant(value: number) {
  return (target: any, key: string) => {
    propertyMapper<number>(target, key, () => {
      return value;
    });
  };
}

class Test {
  @Constant(5)
  constant: number;

  @Constant(10)
  constantInitialized = 34;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor() {

  }

  ngOnInit(): void {
    const test = new Test();
    console.log(test.constant); // Returns 5
    console.log(test.constantInitialized); // Returns 10
    test.constant = 45;
    test.constantInitialized = 45;
    console.log(test.constant); // Returns 5
    console.log(test.constantInitialized); // Returns 10
  }

}

Thus, property decorators is a function with two arguments:

  • Target (Gives you information about the property object instance, here it’s the object ‘Test’)
  • Key (the name of the property)

In this example, each time we access or we set the ‘constant’ property, we trigger the propertyMapper function. If we access the property, we simply returns the value. However, when we set the property, we define a new property by using Object.defineProperty.

Conclusion

  • Decorators are functions
  • Depending of the decorators type, the prototype of the function is different
  • Decorators are hugely used in Angular
  • Use decorators to avoid code duplication

Be the first to comment

Leave a Reply

Your email address will not be published.


*