How to extend / inherit components?

AngularTypescriptInheritanceAngular Directive

Angular Problem Overview


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 changes and wish these changes were also reflected in its derived components.

I created this simple example to try to explain better my questions:

With the following base component app/base-panel.component.ts:

import {Component, Input} from 'angular2/core';

@Component({
	selector: 'base-panel',
	template: '<div class="panel" [style.background-color]="color" (click)="onClick($event)">{{content}}</div>',
	styles: [`
	.panel{
    padding: 50px;
  }
  `]
})
export class BasePanelComponent { 
  
  @Input() content: string;
  
  color: string = "red";
  
  onClick(event){
    console.log("Click color: " + this.color);
  }
}

Would you like to create another derivative component only alter, for example, a basic component behavior in the case of the example color, app/my-panel.component.ts:

import {Component} from 'angular2/core';
import {BasePanelComponent} from './base-panel.component'

@Component({
	selector: 'my-panel',
	template: '<div class="panel" [style.background-color]="color" (click)="onClick($event)">{{content}}</div>',
	styles: [`
	.panel{
    padding: 50px;
  }
  `]
})
export class MyPanelComponent extends BasePanelComponent{
  
  constructor() {
    super();
    this.color = "blue";
  }
}

Complete working example in Plunker

>Note: Obviously this example is simple and could be solved otherwise no need to use inheritance, but it is intended only to illustrate the real problem.

As you can see in the implementation of the derivative component app/my-panel.component.ts, much of the implementation was repeated, and the single part really inherited was the class BasePanelComponent, but the @Component had to basically be completely repeated, not just the changed portions, as the selector: 'my-panel'.

Is there some way to make a literally full inheritance of a component Angular2, inheriting the class definition of the markings/annotations, as for example @Component?

Edit 1 - Feature Request

>Feature request angular2 added to the project on GitHub: Extend/Inherit angular2 components annotations #7968

Edit 2 - Closed Request

>The request was closed, for this reason, that briefly would not know how to merge the decorator will be made. Leaving us with no options. So my opinion is is quoted in the Issue.

Angular Solutions


Solution 1 - Angular

Alternative Solution:

This answer of Thierry Templier is an alternative way to get around the problem.

After some questions with Thierry Templier, I came to the following working example that meets my expectations as an alternative to inheritance limitation mentioned in this question:

1 - Create custom decorator:

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

2 - Base Component with @Component decorator:

@Component({
  // create seletor base for test override property
  selector: 'master',
  template: `
    <div>Test</div>
  `
})
export class AbstractComponent {
  
}

3 - Sub component with @CustomComponent decorator:

@CustomComponent({
  // override property annotation
  //selector: 'sub',
  selector: (parentSelector) => { return parentSelector + 'sub'}
})
export class SubComponent extends AbstractComponent {
  constructor() {
  }
}

Plunkr with complete example.

Solution 2 - Angular

Let us understand some key limitations & features on Angular’s component inheritance system.

The component only inherits the class logic:

  • All meta-data in the @Component decorator is not inherited.
  • Component @Input properties and @Output properties are inherited.
  • Component lifecycle is not inherited.

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.

Solution 3 - Angular

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:

Solution 4 - Angular

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

Solution 5 - Angular

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:

  • Mixins compose more flexibly, i.e. you can mix and match Mixins on existing components or combine Mixins to form new Components
  • Mixin composition remains easy to understand thanks to its obvious linearization to a class inheritance hierarchy
  • You can more easily avoid issues with decorators and annotations that plague component inheritance (discussion)

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.

Solution 6 - Angular

As far as I know component inheritance has not been implemented yet in Angular 2 and I'm not sure if they have plans to, however since Angular 2 is using typescript (if you've decided to go that route) you can use class inheritance by doing class MyClass extends OtherClass { ... }. For component inheritance I'd suggest getting involved with the Angular 2 project by going to https://github.com/angular/angular/issues and submitting a feature request!

Solution 7 - Angular

if you read through the CDK libraries and the material libraries, they're using inheritance but not so much for components themselves, content projection is king IMO. see this link https://blog.angular-university.io/angular-ng-content/ where it says "the key problem with this design"

I know this doesn't answer your question but I really think inheriting / extending components should be avoided. Here's my reasoning:

If the abstract class extended by two or more components contains shared logic: use a service or even create a new typescript class that can be shared between the two components.

If the abstract class... contains shared variables or onClicketc functions, Then there will be duplication between the html of the two extending components views. This is bad practice & that shared html needs to be broken into Component(s). These Component(s) (parts) can be shared between the two components.

Am I missing other reasons for having an abstract class for components?

An example I saw recently was components extending AutoUnsubscribe:

import { Subscription } from 'rxjs';
import { OnDestroy } from '@angular/core';
export abstract class AutoUnsubscribeComponent implements OnDestroy {
  protected infiniteSubscriptions: Array<Subscription>;

  constructor() {
    this.infiniteSubscriptions = [];
  }

  ngOnDestroy() {
    this.infiniteSubscriptions.forEach((subscription) => {
      subscription.unsubscribe();
    });
  }
}

this was bas because throughout a large codebase, infiniteSubscriptions.push() was only used 10 times. Also importing & extending AutoUnsubscribe actually takes more code than just adding mySubscription.unsubscribe() in the ngOnDestroy() method of the component itself, which required additional logic anyway.

Solution 8 - Angular

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!
}

Solution 9 - Angular

Components can be extended as same as a typescript class inheritance, just that you have to override the selector with a new name. All Input() and Output() Properties from the Parent Component works as normal

Update

@Component is a decorator,

Decorators are applied during the declaration of class not on objects.

Basically, decorators add some metadata to the class object and that cannot be accessed via inheritance.

If you want to achieve the Decorator Inheritance I would Suggest writing a custom decorator. Something like below example.

export function CustomComponent(annotation: any) {
    return function (target: Function) {
    var parentTarget = Object.getPrototypeOf(target.prototype).constructor;

    var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);
    var parentParamTypes = Reflect.getMetadata('design:paramtypes', parentTarget);
    var parentPropMetadata = Reflect.getMetadata('propMetadata', parentTarget);
    var parentParameters = Reflect.getMetadata('parameters', parentTarget);

    var parentAnnotation = parentAnnotations[0];

    Object.keys(parentAnnotation).forEach(key => {
    if (isPresent(parentAnnotation[key])) {
        if (!isPresent(annotation[key])) {
        annotation[key] = parentAnnotation[key];
        }
    }
    });
    // Same for the other metadata
    var metadata = new ComponentMetadata(annotation);

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

Refer: https://medium.com/@ttemplier/angular2-decorators-and-class-inheritance-905921dbd1b7

Solution 10 - Angular

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 {
    
}

Solution 11 - Angular

Just use inheritance, extend parent class in child class and declare constructor with parent class parameter and this parameter use in super().

  1. Parent class:
@Component({
  selector: 'teams-players-box',
  templateUrl: '/maxweb/app/app/teams-players-box.component.html'
})
export class TeamsPlayersBoxComponent {
  public _userProfile: UserProfile;
  public _user_img: any;
  public _box_class: string = "about-team teams-blockbox";
  public fullname: string;
  public _index: any;
  public _isView: string;
  indexnumber: number;

  constructor(
    public _userProfilesSvc: UserProfiles,
    public _router: Router,
  ){}
  1. Child class:
@Component({  
  selector: '[teams-players-eligibility]',  
  templateUrl: '/maxweb/app/app/teams-players-eligibility.component.html'  
})  
export class TeamsPlayersEligibilityComponent extends TeamsPlayersBoxComponent {  
  constructor (public _userProfilesSvc: UserProfiles,
    public _router: Router) {  
      super(_userProfilesSvc,_router);  
    }  
  }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionFernando LealView Question on Stackoverflow
Solution 1 - AngularFernando LealView Answer on Stackoverflow
Solution 2 - AngularMasoud BimmarView Answer on Stackoverflow
Solution 3 - AngularDaniel GriscomView Answer on Stackoverflow
Solution 4 - Angularam0waView Answer on Stackoverflow
Solution 5 - AngularJohannes RudolphView Answer on Stackoverflow
Solution 6 - AngularwatzonView Answer on Stackoverflow
Solution 7 - Angularrobert kingView Answer on Stackoverflow
Solution 8 - AngularrhyneavView Answer on Stackoverflow
Solution 9 - AngularMAHESH VALIYA VEETILView Answer on Stackoverflow
Solution 10 - AngularMizuwokiruView Answer on Stackoverflow
Solution 11 - Angularmittal bhattView Answer on Stackoverflow