如何扩展/继承组件?

时间:2016-04-07 12:03:21

标签: angular typescript inheritance angular-directive

我想为已经部署在Angular 2中的一些组件创建扩展,而不必几乎完全重写它们,因为基本组件可能会发生变化,并希望这些变化也反映在其派生组件中。

我创建了这个简单的例子,试图更好地解释我的问题:

使用以下基本组件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);
  }
}

您是否要创建另一个衍生组件,例如,在示例颜色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

  

注意:显然这个例子很简单,可以解决,否则不需要使用继承,但它只是为了说明真正的问题。

正如您在衍生组件app/my-panel.component.ts的实现中所看到的,重复了大部分实现,而真正继承的单个部分是class BasePanelComponent,但是{{ 1}}基本上必须完全重复,而不仅仅是@Component

是否有某种方法可以对组件Angular2进行字面上的完全继承,继承标记/注释的selector: 'my-panel'定义,例如class

修改1 - 功能请求

  

在GitHub上添加到项目的功能请求angular2:Extend/Inherit angular2 components annotations #7968

编辑2 - 已结束请求

  

请求已关闭,for this reason,暂时不知道如何合并装饰器。离开我们没有选择。所以我的观点是quoted in the Issue

12 个答案:

答案 0 :(得分:28)

替代解决方案:

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

在与Thierry Templier的一些问题之后,我来到了以下工作示例,该示例符合我的期望,作为此问题中提到的继承限制的替代方案:

1 - 创建自定义装饰器:

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 - 带@Component装饰器的基础组件:

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

}

3 - 带@CustomComponent装饰器的子组件:

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

Plunkr with complete example.

答案 1 :(得分:25)

Angular 2 2.3版刚刚发布,它包含本机组件继承。看起来你可以继承和覆盖你想要的任何东西,模板和样式除外。一些参考文献:

答案 2 :(得分:17)

既然TypeScript 2.2支持Mixins through Class expressions,我们就可以更好地表达Mixins on Components。请注意,您也可以使用组件继承,因为角度为2.3(discussion)或自定义装饰器,如此处其他答案中所述。但是,我认为Mixins有一些属性使它们更适合重用组件之间的行为:

  • Mixins组成更灵活,即您可以在现有组件上混合搭配Mixins或将Mixins组合成新组件
  • 由于对类继承层次结构的明显线性化,Mixin组合仍然易于理解
  • 您可以更轻松地避免使用装饰器和注释组件继承的注释(discussion

我强烈建议您阅读上面的TypeScript 2.2声明,了解Mixins的工作原理。角度GitHub问题中的链接讨论提供了更多细节。

您需要以下类型:

export type Constructor<T> = new (...args: any[]) => T;

export class MixinRoot {
}

然后你可以声明像这样Destroyable mixin的Mixin,它可以帮助组件跟踪需要在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
    }
  };
}

要将Destroyable混合到Component中,您可以声明您的组件:

export class DashboardComponent extends Destroyable(MixinRoot) 
    implements OnInit, OnDestroy { ... }

请注意,MixinRoot仅在您想要extend Mixin合成时才需要。您可以轻松扩展多个mixin,例如A extends B(C(D))。这是我上面谈到的mixins的明显线性化,例如你正在有效地组成一个继承层次结构A -> B -> C -> D

在其他情况下,例如当你想在现有的类上组合Mixins时,你可以像这样应用Mixin:

const MyClassWithMixin = MyMixin(MyClass);

但是,我发现第一种方式最适合ComponentsDirectives,因为这些方法也需要用@Component@Directive进行修饰。

答案 3 :(得分:14)

<强>更新

2.3.0-rc.0

以来支持组件继承

<强>原始

到目前为止,对我来说最方便的是保留模板&amp;样式分为*html&amp; *.csstemplateUrl个文件,并通过styleUrls@Component { selector: 'my-panel', templateUrl: 'app/components/panel.html', styleUrls: ['app/components/panel.css'] } export class MyPanelComponent extends BasePanelComponent 指定这些文件,因此可以轻松重复使用。

a

答案 4 :(得分:10)

据我所知,组件继承尚未在Angular 2中实现,我不确定他们是否有计划,但是由于Angular 2正在使用打字稿(如果你决定走这条路线),你可以通过执行class MyClass extends OtherClass { ... }来使用类继承。对于组件继承,我建议通过转到https://github.com/angular/angular/issues并提交功能请求来参与Angular 2项目!

答案 5 :(得分:4)

我知道这并没有回答你的问题但我真的认为应该避免继承/扩展组件。这是我的理由:

如果由两个或多个组件扩展的抽象类包含共享逻辑: 使用服务甚至创建一个可以在两个组件之间共享的新打字稿类。

如果抽象类...包含共享变量或onClicketc函数, 然后两个扩展组件视图的html之间会有重复。这是不好的做法&amp;共享html需要分解为组件。这些组件(部件)可以在两个组件之间共享。

我是否遗漏了为组件设计抽象类的其他原因?

我最近看到的一个例子是扩展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();
    });
  }
}

这是bas,因为在整个大型代码库中,infiniteSubscriptions.push()只使用了10次。还导入&amp;扩展AutoUnsubscribe实际上需要的代码多于在组件本身的ngOnDestroy()方法中添加mySubscription.unsubscribe(),这无论如何都需要额外的逻辑。

答案 6 :(得分:3)

您可以继承@ Input,@ Output,@ ViewChild等。查看示例:

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

}

答案 7 :(得分:2)

如果有人在寻找更新的解决方案,费尔南多的答案非常完美。除了ComponentMetadata已被弃用。使用Component代替我工作。

完整的Custom Decorator CustomDecorator.ts文件如下所示:

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

然后将其导入新的组件sub-component.component.ts文件并使用@CustomComponent代替@Component,如下所示:

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

...

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

  constructor() {
    super();
  }

  // Add new logic here!
}

答案 8 :(得分:1)

组件可以像typescript类继承一样扩展,只需要用新名称覆盖选择器。父组件中的所有Input()和Output()属性都正常工作

<强>更新

@Component是装饰者,

装饰器在类声明期间应用,而不是在对象上。

基本上,装饰器会向类对象添加一些元数据,而这些元数据不能通过继承来访问。

如果你想实现Decorator继承,我会建议编写一个自定义装饰器。像下面的例子。

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

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

答案 9 :(得分:0)

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,
    ){}
2.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);
        }
}

答案 10 :(得分:0)

让我们了解Angular组件继承系统的一些关键限制和功能。

该组件仅继承类逻辑 @Component装饰器中的所有元数据都不继承 组件@Input属性和@Output属性是继承的 组件生命周期未继承 要牢记这些功能非常重要,因此让我们独立检查每个功能。

该组件仅继承类逻辑 当您继承Component时,内部的所有逻辑均被同等继承。值得注意的是,只有公共成员才能继承,因为私有成员只能在实现它们的类中访问。 @Component装饰器中的所有元数据均未继承 起初没有继承元数据的事实乍看起来似乎是违反直觉的,但是,如果考虑到这一点,这实际上是很合理的。如果从Component(组件A)继承,则不希望从中继承ComponentA的选择器替换继承的类ComponentB的选择器。对于template / templateUrl以及style / styleUrls都可以这样说。

组件的@Input和@Output属性是继承的

这是我非常喜欢Angular中组件继承的另一个功能。简单地说,只要您具有自定义的@Input和@Ouput属性,这些属性就会被继承。

组件生命周期未继承 这部分是不太明显的部分,特别是对于尚未广泛使用OOP原理的人们。例如,假设您有ComponentA,它实现了Angular的许多生命周期挂钩之一,例如OnInit。如果创建ComponentB并继承ComponentA,则即使您确实具有ComponentB的OnInit生命周期,也要在您显式调用它之前从ComponentA触发OnInit生命周期。

调用超级/基本组件方法 为了使ComponentA的ngOnInit()方法生效,我们需要使用super关键字,然后调用所需的方法(在本例中为ngOnInit)。 super关键字是指要继承的组件的实例,在这种情况下将是ComponentA。

答案 11 :(得分:-4)

您可以使用<ng-content></ng-content>指令。

例如:

标题组件

@Component({
      selector: 'header-component',
      template: `
                 <header>
                     <ng-content><ng-content>
                 </header>
                `
})
export class HeaderComponent {

}

自定义标题组件

@Component({
      selector: 'custom-header-component',
      template: `
                 <header-component>
                     This is custom header component
                     <ng-content></ng-content>
                 </header-component>
                `
})
export class CustomHeaderComponent {

}

<header-component>This is header</header-component>打印这是标题

<custom-header-component>This is header</custom-header-component>打印这是自定义标头 这是标题