I would like to create extensions for some components already deployed in Angular 2, without having to rewrite them almost completely, as the base component could undergo ch
Now that TypeScript 2.2 supports Mixins through Class expressions we have a much better way to express Mixins on Components. Mind you that you can also use Component inheritance since angular 2.3 (discussion) or a custom decorator as discussed in other answers here. However, I think Mixins have some properties that make them preferable for reusing behavior across components:
I strongly suggest you read the TypeScript 2.2 announcement above to understand how Mixins work. The linked discussions in angular GitHub issues provide additional detail.
You'll need these types:
export type Constructor<T> = new (...args: any[]) => T;
export class MixinRoot {
}
And then you can declare a Mixin like this Destroyable
mixin that helps components keep track of subscriptions that need to be disposed in ngOnDestroy
:
export function Destroyable<T extends Constructor<{}>>(Base: T) {
return class Mixin extends Base implements OnDestroy {
private readonly subscriptions: Subscription[] = [];
protected registerSubscription(sub: Subscription) {
this.subscriptions.push(sub);
}
public ngOnDestroy() {
this.subscriptions.forEach(x => x.unsubscribe());
this.subscriptions.length = 0; // release memory
}
};
}
To mixin Destroyable
into a Component
, you declare your component like this:
export class DashboardComponent extends Destroyable(MixinRoot)
implements OnInit, OnDestroy { ... }
Note that MixinRoot
is only necessary when you want to extend
a Mixin composition. You can easily extend multiple mixins e.g. A extends B(C(D))
. This is the obvious linearization of mixins I was talking about above, e.g. you're effectively composing an inheritnace hierarchy A -> B -> C -> D
.
In other cases, e.g. when you want to compose Mixins on an existing class, you can apply the Mixin like so:
const MyClassWithMixin = MyMixin(MyClass);
However, I found the first way works best for Components
and Directives
, as these also need to be decorated with @Component
or @Directive
anyway.
update
Component inheritance is supported since 2.3.0-rc.0
original
So far, the most convenient for me is to keep template & styles into separate *html
& *.css
files and specify those through templateUrl
and styleUrls
, so it's easy reusable.
@Component {
selector: 'my-panel',
templateUrl: 'app/components/panel.html',
styleUrls: ['app/components/panel.css']
}
export class MyPanelComponent extends BasePanelComponent
You can inherit @Input, @Output, @ViewChild, etc. Look at the sample:
@Component({
template: ''
})
export class BaseComponent {
@Input() someInput: any = 'something';
@Output() someOutput: EventEmitter<void> = new EventEmitter<void>();
}
@Component({
selector: 'app-derived',
template: '<div (click)="someOutput.emit()">{{someInput}}</div>',
providers: [
{ provide: BaseComponent, useExisting: DerivedComponent }
]
})
export class DerivedComponent {
}
Let us understand some key limitations & features on Angular’s component inheritance system.
The component only inherits the class logic:
These features are very important to have in mind so let us examine each one independently.
The Component only inherits the class logic
When you inherit a Component, all logic inside is equally inherited. It is worth noting that only public members are inherited as private members are only accessible in the class that implements them.
All meta-data in the @Component decorator is not inherited
The fact that no meta-data is inherited might seem counter-intuitive at first but, if you think about this it actually makes perfect sense. If you inherit from a Component say (componentA), you would not want the selector of ComponentA, which you are inheriting from to replace the selector of ComponentB which is the class that is inheriting. The same can be said for the template/templateUrl as well as the style/styleUrls.
Component @Input and @Output properties are inherited
This is another feature that I really love about component Inheritance in Angular. In a simple sentence, whenever you have a custom @Input and @Output property, these properties get inherited.
Component lifecycle is not inherited
This part is the one that is not so obvious especially to people who have not extensively worked with OOP principles. For example, say you have ComponentA which implements one of Angular’s many lifecycle hooks like OnInit. If you create ComponentB and inherit ComponentA, the OnInit lifecycle from ComponentA won't fire until you explicitly call it even if you do have this OnInit lifecycle for ComponentB.
Calling Super/Base Component Methods
In order to have the ngOnInit() method from ComponentA fire, we need to use the super keyword and then call the method we need which in this case is ngOnInit. The super keyword refers to the instance of the component that is being inherited from which in this case will be ComponentA.
Angular 2 version 2.3 was just released, and it includes native component inheritance. It looks like you can inherit and override whatever you want, except for templates and styles. Some references:
New features in Angular 2.3
Component Inheritance in Angular 2
A Plunkr demonstrating component inheritance
If anyone is looking for an updated solution, Fernando'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!
}