Our content is free thanks to ag-Grid

ag-Grid is the industry leading JavaScript datagrid

ag-grid.com

Implementing custom component decorator in Angular

Post Editor

Let's take a look under the hood of the component decorator mechanism in Angular. We'll learn how it works in JavaScript, why Angular uses it and how to implement a custom one.

3 min read
post

Implementing custom component decorator in Angular

Let's take a look under the hood of the component decorator mechanism in Angular. We'll learn how it works in JavaScript, why Angular uses it and how to implement a custom one.

post
post
3 min read
3 min read

Angular uses two JavaScript capabilities that are currently being standardized — decorators and metadata reflection API to allow declarative components definition. Currently they are not supported by the JS but both are on their track to become available in our browsers very soon. And since these features are not yet supported, Angular uses TypeScript compiler to allow the usage of decorators and reflect-metadata npm package to shim metadata reflection API. There is a great deal of articles on the web that describe decorators and metadata API in details. This article shows how they are used in Angular.

Whenever we need to define a new component in Angular we use @Component decorator like this:

@Component({
  selector: 'my-app',
  template: '<span>I am a component</span>',
})
export class AppComponent {
  name = 'Angular';
}

The spec defines a decorator as an expression that evaluates to a function that takes the target, name, and decorator descriptor as arguments. What does “A decorator evaluates to a function” mean? It simply means that you can use a decorator by itself:

@isTestable
class MyClass { }

function isTestable(target) {
   target.isTestable = true;
}

or as a wrapper function usually called “decorator factory” that returns a decorator function:

@isTestable(true)
class MyClass { }

function isTestable(value) {
   return function decorator(target) {
      target.isTestable = value;
   }
}

All angular decorators use the second approach with a wrapper function. The core functionality of most angular decorators is to attach metadata to a class. This metadata is then used by the compiler to construct various factories.

To better understand the concept let’s implement custom decorator to define components. The first thing we need to know is what all possible component decorator properties exist. And they are defined here:

export const defaultComponentProps = {
  selector: undefined,
  inputs: undefined,
  outputs: undefined,
  host: undefined,
  exportAs: undefined,
  moduleId: undefined,
  providers: undefined,
  viewProviders: undefined,
  changeDetection: ChangeDetectionStrategy.Default,
  queries: undefined,
  templateUrl: undefined,
  template: undefined,
  styleUrls: undefined,
  styles: undefined,
  animations: undefined,
  encapsulation: undefined,
  interpolation: undefined,
  entryComponents: undefined
};

I mentioned earlier that Angular uses wrapper function approach to decorators. This wrapper function takes component properties and merges them with the defaults. Let’s implement that:

export function CustomComponentDecorator(_props) {
  _props = Object.assign({}, defaultProps, _props);
  return function (cls) { }
}

I also mentioned that the single purpose of a decorator in Angular is to attach metadata to a class. A metadata is simply a merge result of defaults with the specified properties. Angular expects that there is a global object Reflect with the API to define and retrieve metadata. Let’s use this information and modify our implementation a bit:

const Reflect = global['Reflect'];
export function CustomComponentDecorator(_props) {
  _props = Object.assign({}, defaultProps, _props);

  return function (cls) {
    Reflect.defineMetadata('annotations', [_props], cls);
  }
}

This is the essence of a @Component decorator. We can now use our custom decorator instead of the one provided by the framework:

@CustomComponentDecorator({
  selector: 'my-app',
  template: '<span>I am a component</span>',
})
export class AppComponent {
  name = 'Angular';
}

However, if you run the application, you’ll get an error:

Unexpected value ‘AppComponent’ declared by the module ‘AppModule’. Please add a @Pipe/@Directive/@Component annotation.

This error occurs because Angular uses runtime check for each metadata instance to verify it was constructed using the appropriate decorators. Since we used our custom decorator, the check didn’t pass. Luckily for us, the check is very simple:

function isDirectiveMetadata(type: any): type is Directive {
  return type instanceof Directive;
}

It just checks whether a metadata instance inherits from the DecoratorFactory. It’s a private function and we can’t import it. However, we can use our knowledge of JavaScript to obtain it from any decorated metadata:

const c = class c {};
Component({})(c);
const DecoratorFactory = Object.getPrototypeOf(Reflect.getOwnMetadata('annotations', c)[0]);

With DecoratorFactory in hand, we need to simply setup correct inheritance in our custom decorator function:

export function CustomComponentDecorator(_props) {
  _props = Object.assign({}, defaultProps, _props);
  Object.setPrototypeOf(_props, DecoratorFactory);

  return function (cls) {
    Reflect.defineMetadata('annotations', [_props], cls);
  }
}

We can also rewrite the above code using Object.create for performance reasons:

export function CustomComponentDecorator(_props) {
  let props = Object.create(DecoratorFactory);
  props = Object.assign(props, defaultComponentProps, _props);

  return function (cls) {
    Reflect.defineMetadata('annotations', [props], cls);
  }
}

Voilà, we have our own working equivalent of built-in @Component decorator.

Discuss with community

Share

About the author

author_image
Max Koretskyi

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

author_image

About the author

Max Koretskyi

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

About the author

author_image
Max Koretskyi

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

RxJSpost
21 January 20214 min read
RxJS in Angular: Part III

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

RxJSpost
21 January 20214 min read
RxJS in Angular: Part III

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

Read more
RxJSpostRxJS in Angular: Part III

21 January 2021

4 min read

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

Read more
Angularpost
20 January 20216 min read
Angular and SOLID principles

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Angularpost
20 January 20216 min read
Angular and SOLID principles

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Read more
AngularpostAngular and SOLID principles

20 January 2021

6 min read

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Read more
Angularpost
14 January 20216 min read
Demystifying Taiga UI root component: portals pattern in Angular

Just before new year we announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.