27

I have several component decorator declarations that I repeat on every component, for example:

@Component({
    moduleId: module.id,
    directives: [BootstrapInputDirective]
})

How can I apply these declarations to all my components? I tried to create a base class with this decorator and extend other classes with it but base class decorations doesn't seem to apply to derivative classes.

dstr
  • 8,362
  • 12
  • 66
  • 106

6 Answers6

30

@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:

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Is it possible to implement my own Component decorator that extends @Component which has a my own default value for a property like moduleId? – dstr Apr 25 '16 at 10:48
  • @ThierryTemplier, In my scenario I need to override some attributes in the base component, then adapted his [example](https://plnkr.co/edit/3JPtDhBns9gqVkeW2jhm?p=preview). You could analyze the changes implemented for testing for my scenario? The changed lines has reviews with the marking: `CHECK:`. What basically tried was permiter override annotation properties and have access to property of component base to merge in Subcomponent statement (failed =/) – Fernando Leal May 30 '16 at 17:48
  • @Fernando what do you mean by access to property of component? property specified in the decorator? (static or not) property of the class? – Thierry Templier May 30 '16 at 18:26
  • @ThierryTemplier, property of decorator, for example, the property `selector` of `@Component` of base class. – Fernando Leal May 30 '16 at 18:43
  • @Fernando: Yes this could be possible by specifying function for properties in the decorator. Something like that: `select: (parentSelector) => parentSelector + 'sub',`. See this plunkr for answers to your checks: https://plnkr.co/edit/TuEccHRUOkpB49n9D8ry?p=preview. – Thierry Templier May 30 '16 at 19:00
  • 1
    @ThierryTemplier, I believe it was something like [this](https://plnkr.co/edit/hw74eB7XuOxmd49WCHQa?p=preview) that you suggested in relation to the function decorator, where it checks if the property is a `function` and if the `function` is run through the base value as a parameter. Correct? I believe that with a few more adjustments such as: Multi inheritance levels (N levels), inject directives more than one level in the same context, merge resolution in properties, etc. Something like this can be suggested in [this issue of Angular2 project](https://github.com/angular/angular/issues/7968)? – Fernando Leal May 30 '16 at 20:05
  • 1
    @Fernando I had a look at your plunkr and it's exactly what I had in mind ;-) Interesting ideas! I'm working on an article on this so I could implement this and add it to the article. I'll let you know... – Thierry Templier May 31 '16 at 18:46
  • lang.js:14Uncaught SyntaxError: Unexpected token export –  Jul 21 '16 at 20:17
  • @xero could you tell me more about your environment? (typescript, es6, es5, angular2 version, ...) Thanks! – Thierry Templier Jul 22 '16 at 07:14
  • 3
    ComponentMetadata is not in core anymore... is there a way to use this in 2.1.0? – DS_web_developer Oct 17 '16 at 15:06
  • Second on the ComponentMetadata. I don't know how to update this for it to continue to work. – jmilloy Dec 21 '16 at 19:12
  • The `provide` function seems not to exist in `angular2/core` anymore. The syntax is `{provide: PLATFORM_DIRECTIVE, useValue: (...)}` [see this issue](https://github.com/angular/angular/issues/8580#issuecomment-273242271) – kub1x Jan 17 '17 at 17:49
9

Just in case you are looking for isPresent function:

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

dipcore
  • 190
  • 2
  • 8
8

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];
    });
};

}

Guinnberg
  • 81
  • 1
  • 3
8

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!
}
rhyneav
  • 125
  • 1
  • 4
  • Do you have a Plunker for this? I am trying this but not working, Check this [Plunker](http://plnkr.co/edit/7TzSmryj83A7Pq6cHE8m?p=preview). – Madhu Ranjan Mar 14 '17 at 16:44
  • I briefly tried to make a Plunker, but it looks like Plunker isn't able to find isPresent from @angular/platform-browser/src/facade/lang (it throws a 500 error). This custom component is used to inherit information from its parent. This is useful for if you want to use the template of another component, but use different TypeScript functions. Also, make sure you are extending your child component: `export class ChildComponent extends ParentComponent`. This way Angular knows what parent to look for! – rhyneav Mar 14 '17 at 19:30
  • 2
    If you just use `@angular/platform-browser` Plunker is able to find `isPresent ` , I am facing issue at `Reflect.getMetadata('annotations', parentTarget)` it returns null object. – Madhu Ranjan Mar 14 '17 at 19:33
  • 1
    Ohhh, good to know, thanks! That issue might be where `import 'zone.js';` & `import 'reflect-metadata';` might be needed. Do you know if there is a way to access these in Plunker? – rhyneav Mar 14 '17 at 19:44
2

Thierry Templier's solution will not propably work from Angular 9+ check (Ivy, AOT) https://github.com/angular/angular/issues/31495

Matej Janotka
  • 302
  • 3
  • 9
1

You can provide services globally in your bootstrap function like:

bootstrap(AppComponent, [HTTP_PROVIDERS, provide(SharedService, {useValue: sharedService})]);

where sharedService is your imported service.

eko
  • 39,722
  • 10
  • 72
  • 98