Extending component decorator with base class decorator

Solution 1:

@Component is a decorator. This means that it handles the class it applies on by adding some metadata data leveraging the reflect-metadata library. Angular2 doesn't look for metadata on parent classes. For this reason, it's not possible to use decorators on parent classes.

Regarding the BootstrapInputDirective directive, you could define it as a platform one. This way you wouldn't need to include it each time into the directives attribute of your components.

Here is a sample:

(...)
import {PLATFORM_DIRECTIVES} from 'angular2/core';

bootstrap(AppComponent, [
  provide(PLATFORM_DIRECTIVES, {useValue: [BootstrapInputDirective], multi:true})
]);

Edit

Yes, you could create your own decorator to implement this. Here is a sample:

export function CustomComponent(annotation: any) {
  return function (target: Function) {
    var parentTarget = annotation.parent;
    delete annotation.parent;
    var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);

    var parentAnnotation = parentAnnotations[0];
    Object.keys(parentAnnotation).forEach(key => {
      if (isPresent(parentAnnotation[key])) {
        annotation[key] = parentAnnotation[key];
      }
    });
    var metadata = new ComponentMetadata(annotation);

    Reflect.defineMetadata('annotations', [ metadata ], target);
  }
}

The CustomComponent decorator will be used this way:

@Component({
  template: `
    <div>Test</div>
  `
})
export class AbstractComponent {
}

@CustomComponent({
  selector: 'sub',
  parent: AbstractComponent
})
export class SubComponent extends AbstractComponent {
}

Note that we need to provide the parent class as input of the decorator since we can find out this parent class within the decorator. Only the prototype of this class but the metadata are applied on the class and not on the associated prototype by reflect-metadata.

Edit2

Thanks to Nitzam's answer, here is an improvment:

export function CustomComponent(annotation: any) {
  return function (target: Function) {
    var parentTarget = Object.getPrototypeOf(target.prototype).constructor;
    var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);

    var parentAnnotation = parentAnnotations[0];
    Object.keys(parentAnnotation).forEach(key => {
      if (isPresent(parentAnnotation[key])) {
        annotation[key] = parentAnnotation[key];
      }
    });
    var metadata = new ComponentMetadata(annotation);

    Reflect.defineMetadata('annotations', [ metadata ], target);
  }
}

There is no need for a parent attribute to reference the parent class in the custom decorator.

See this plunkr: https://plnkr.co/edit/ks1iK41sIBFlYDb4aTHG?p=preview.

See this question:

  • How to get the parent class at runtime

Solution 2:

After latest releases of Angular, the ComponentMetadata class isn't available as pointed out by few members here.

This is how I've implemented the CustomComponent to make it work:

export function CustomComponent(annotation: any) {
  return function (target: Function) {
      let parentTarget = Object.getPrototypeOf(target.prototype).constructor;
      let parentAnnotations = Reflect.getOwnMetadata('annotations', parentTarget);

      let parentAnnotation = parentAnnotations[0];

      Object.keys(annotation).forEach(key => {
        parentAnnotation[key] = annotation[key];
      });
  };
}

Hope it helps!

EDIT: the previous chunk of code, even if it works, it overrides the original metadata of the extended class. Find below an enhanced version of it, allowing you to have multiple inheritances and overrides without modifying the base class.

export function ExtendComponent(annotation: any) {
  return function (target: Function) {
    let currentTarget = target.prototype.constructor;
    let parentTarget = Object.getPrototypeOf(target.prototype).constructor;
    let parentAnnotations = Reflect.getOwnMetadata('annotations', parentTarget);

    Reflect.defineMetadata('annotations', [Object.create(parentAnnotations[0])], currentTarget);
    let currentAnnotations = Reflect.getOwnMetadata('annotations', currentTarget);

    Object.keys(annotation).forEach(key => {
        currentAnnotations[0][key] = annotation[key];
    });
};

}

Solution 3:

Just in case you are looking for isPresent function:

function isPresent(obj: any): boolean { return obj !== undefined && obj !== null; }

Solution 4:

If anyone is looking for an updated solution, Thierry Templier's answer is pretty much perfect. Except that ComponentMetadata has been deprecated. Using Component instead worked for me.

The full Custom Decorator CustomDecorator.ts file looks like this:

import 'zone.js';
import 'reflect-metadata';
import { Component } from '@angular/core';
import { isPresent } from "@angular/platform-browser/src/facade/lang";

export function CustomComponent(annotation: any) {
  return function (target: Function) {
    var parentTarget = Object.getPrototypeOf(target.prototype).constructor;
    var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);

    var parentAnnotation = parentAnnotations[0];
    Object.keys(parentAnnotation).forEach(key => {
      if (isPresent(parentAnnotation[key])) {
        // verify is annotation typeof function
        if(typeof annotation[key] === 'function'){
          annotation[key] = annotation[key].call(this, parentAnnotation[key]);
        }else if(
          // force override in annotation base
          !isPresent(annotation[key])
        ){
          annotation[key] = parentAnnotation[key];
        }
      }
    });

    var metadata = new Component(annotation);

    Reflect.defineMetadata('annotations', [ metadata ], target);
  }
}

Then import it in to your new component sub-component.component.ts file and use @CustomComponent instead of @Component like this:

import { CustomComponent } from './CustomDecorator';
import { AbstractComponent } from 'path/to/file';

...

@CustomComponent({
  selector: 'subcomponent'
})
export class SubComponent extends AbstractComponent {

  constructor() {
    super();
  }

  // Add new logic here!
}