Extending the Component decorator in Angular 6

Decorators

Decorators are an excellent way to add metadata information to a class / method / property / parameter. In C# language they are called as attributes and implemented as classes.

public class Employee
{
    [Required]
    [MaxLength(50)]
    public string Name { get; set; }

    [Required]
    public int Age { get;set; }
}

Listing 1. Attributes in C#

In the above C# example, attributes (Required and MaxLength) are used to specify the validation rules for the properties of an Employee class. These validation rules are very much connected to a model and it makes sense to specify them as part of the class itself. Without attributes/decorators also you can specify the rules but it's not easy and efficient like them.

TypeScript Decorators

In TypeScript, decorators are implemented using functions. Actually it's a function that returns a function. The outside function is called as decorator factory and the inside one is called as the decorator. The decorator factory takes some configuration values and returns a decorator.

The below decorator is used to represent a class as JSON serializable. It defines a property called isSerializable in the type with value true and adds a method to the prototype to serialize the instances into JSON string.

function Serializable(replacer?: (key: string, value: any) => any, space?: string | number) {
    return function (type) {
       Object.defineProperty(type, 'isSerializable',  { value: true });
       Object.defineProperty(type.prototype, 'serialize', {
            value:  function () {
                return JSON.stringify(this, replacer, space);
            }
       });
    }
}

Listing 2. A simple decorator in TypeScript

@Serializable(null, 2)
class Employee {
    constructor(public name: string, public age: number) {}
}

console.log((Employee).isSerializable); // true

var emp: any = new Employee('Vijay', 35);
var str = emp.serialize();
console.log(str);
// Output
{
  "name": "Vijay",
  "age": 35
}

Listing 3. Using the sample decorator

Angular Decorators

Angular uses quite a lot of decorators. There are decorators for classes, properties, methods and even parameters. Some of the important decorators are:

  • NgModule
  • Component
  • Input
  • Output

The Component decorator is used to decorate a class as an angular component and adds additional metadata information like the template, selector name, styles etc. to it.

@Component({
   selector: 'greet',
   template: '

{{greeting}}

}) class GreetingComponent { @Input() public greeting: string; }

Listing 4. A simple angular component

What Component decorator does?

The Component decorator does the following things.

  • Creates an instance of a function called DecoratorFactory (derived from Directive).
  • Fills that instance with the passed arguments (selector, template etc).
  • Defines a static property in the component type with name "__annotations__" and assign the instance to it.

To verify this, please run the below statement.

console.log(GreetingComponent['__annotations__']);

Listing 5. Displaying GreetingComponent's Annotations

You'll see the below output.

GreetingComponent annotations

GreetingComponent's annotations

Extending the built-in Component decorator

Sometimes you need to pass additional options to the Component decorator, read them and act accordingly.

In my recent project, all the components are model driven and I need to register them to a custom registry with a friendly name and the associated model type. To accomplish this I need to pass some additional options like "name" and "model" to the Component decorator.

For example let's say we've a model and component class as below,

class ButtonModel {
}

@Component({
    selector: 'btn',
    templateUrl: './button.html
})
class ButtonComponent {
}

Listing 6. A sample component with model

Without decorators I would be doing like this,

componentRegistry.register('button', ButtonComponent, ButtonModel);

Listing 7. Registering the component and the model

It would be great if I do the registration as part of the decorator. Basically I would like to do something like below,

@MyComponent({
    selector: 'btn',
    templateUrl: './button.html,
    name: 'button',
    model: 'ButtonModel'
})
class ButtonComponent {
}

Listing 8. Passing additional options to decorator

Note, I've used my own decorator called MyComponent and I've passed additional options called "name" and "model" that are required for registration. I don't want to implement all the functionalities of Component decorator in MyComponent instead I want it to wrap the built-in one.

Let's see the implementation of MyComponent decorator.

import { Component } from '@angular/core';
import componentRegistry from './ComponentRegistry';

function MyComponent(args: any = {}): (cls: any) => any {
  const ngCompDecorator = Component(args);

  return function (compType: any) {
    ngCompDecorator(compType);
    args.type && componentRegistry.register(args.type, compType, args.model);
  }
}

export default MyComponent;

Listing 9. Custom decorator

In the above code you can see I've first invoked the built-in angular Component decorator factory and stored the returned decorator function in a variable. In my actual decorator function I've invoked Component decorator function first and then wrote my custom code that reads the additional options from the passed arguments and registers the component. That's the beauty of decorators, you can decorate a decorator!

Summary

Decorators are powerful ways to add additional info to a class, property or method. JavaScript not yet supports them and possibly the future version might. Luckily we can use them right now with TypeScript.

In the next post I'll show you how we can extend the angular property decorator @Input.

blog comments powered by Disqus