如何扩展/继承组件?


160

我想为已经部署在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";
  }
}

Plunker中的完整工作示例

注意:显然,此示例很简单,可以解决,否则无需使用继承,但是它仅用于说明实际问题。

正如您在派生组件的实现中看到的那样app/my-panel.component.ts,重复了很多实现,真正继承的单个部分是class BasePanelComponent,但@Component必须基本上完全重复,而不仅仅是更改部分selector: 'my-panel'

例如,是否有某种方法可以使Angular2组件真正地完全继承,例如继承class标记/注释的定义@Component

编辑1-功能请求

在GitHub上的项目中添加了功能请求angular2:扩展/继承angular2组件注释#7968

编辑2-已关闭的请求

由于这个原因,该请求已关闭,即短暂地将不知道如何合并装饰器。别无选择。因此,我在Issue中引用了我的观点。



好的,尼古拉斯·B。但是我的问题是装饰器@Component的继承,这不适用于继承元数据。= /
Fernando Leal

人们,请避免将继承与angular一起使用。例如,导出类PlannedFilterComponent扩展了AbstractFilterComponent实现的OnInit {是非常糟糕的。还有其他共享代码的方式,例如服务和较小的组件。继承不是有角度的方法。我在一个有角度的项目中,他们使用了继承,并且发生了一些中断,例如导出从缺少抽象类输入的抽象组件继承的组件。
罗伯特·金

Answers:


39

替代解决方案:

Thierry Templier的答案是解决问题的另一种方法。

在与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。


3
我认为这将与离线模板编译器不兼容。
君特Zöchbauer

@GünterZöchbauer,我对Angular2的“离线编译器模板”一无所知。但是我认为这可能不兼容,这将是一个替代选择。Angular2的“离线模板编译器”模式在哪里有用?您可以向我展示一些可以更好地了解这一点的东西吗?因此,我可以了解这种兼容性对我的项目的重要性。
Fernando Leal

脱机模板编译器(OTC)尚未运行,即使它已包含在RC.3中。在生成可部署项目时,OTC将分析装饰器并在构建步骤中生成代码。OTC允许删除在运行时处理装饰器和绑定的Angular2解析器和编译器,从而显着减少代码大小并加快应用程序和组件的初始化。OTC可能会在下一个更新之一中变得可用。
君特Zöchbauer

1
@GünterZöchbauer,现在我明白了保持与OTC兼容的功能的重要性。这将是角度装饰器的预编译,可减少初始化组件的开销。我想知道此过程的功能,并且因为此答案的解决方案与OTC不兼容?装饰器的预编译如何?掌握了这些知识后,我们可能会想出一些办法来保留此功能以替代OTC。谢谢你的澄清!
Fernando Leal

24

Angular 2 2.3版刚刚发布,它包含本机组件继承。看起来,您可以继承和覆盖所需的任何内容,模板和样式除外。一些参考:


当您忘记在子组件中指定一个新的“选择器”时,就会出现一个“陷阱”。如果没有,则会出现运行时错误More than one component matched on this element
Aelfinn

@TheAelfinn是的:每个组件必须在@Component()标签中具有完整的规格。但是,如果需要,您可以通过引用同一文件来共享.html或.css。总而言之,这是一大优势
丹尼尔·格里斯康

在您的第二个链接scotch.io/tutorials/component-inheritance-in-angular-2中,作者声称组件继承了其父级的依赖项注入服务,而我的代码则提出了其他建议。您可以确认支持吗?
Aelfinn

18

现在TypeScript 2.2 通过Class表达式支持Mixins,我们有了一种更好的方式来在Components上表达Mixins。请注意,您也可以使用从angular 2.3(讨论)开始的组件继承,或者使用此处其他答案中讨论的自定义装饰器。但是,我认为Mixins具有一些属性,使其更适合跨组件重用:

  • Mixins的组合更加灵活,例如,您可以在现有组件上混合和匹配Mixins或将Mixins组合成新的组件
  • Mixin组成由于对类继承层次结构的明显线性化而仍然易于理解
  • 您可以更轻松地避免装饰器和注解问题困扰组件继承(讨论

我强烈建议您阅读上面的TypeScript 2.2公告,以了解Mixins的工作方式。Angular GitHub问题中的链接讨论提供了更多详细信息。

您将需要以下类型:

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

export class MixinRoot {
}

然后,您可以像这样的Destroyablemixin 声明一个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
    }
  };
}

要混入DestroyableComponent,你宣布你这样的组件:

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

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

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

const MyClassWithMixin = MyMixin(MyClass);

但是,我发现第一种方法最适合ComponentsDirectives,因为它们也都需要用@Component或修饰@Directive


这太棒了!谢谢你的建议。MixinRoot只是在这里用作空类占位符吗?只想确保我的理解是正确的。
亚历克斯·洛克伍德

@AlexLockwood是的,空类占位符正是我使用它的目的。我会很乐意避免使用它,但是到目前为止,我还没有找到更好的方法来使用它。
约翰内斯·鲁道夫

2
我最终使用function Destroyable<T extends Constructor<{}>>(Base = class { } as T)。这样,我可以使用创建混入extends Destroyable()
亚历克斯·洛克伍德

1
这看起来非常好,但是AoT构建(Cli1.3)似乎从未从DashBoardComponent中删除ngOnDestroy,因为它从未被调用。(ngOnInit也一样)
dzolnjan

感谢您的解决方案。但是,在使用离子或angular-cli构建产品之后,mixin无法以某种方式工作,就好像没有扩展一样。
韩彻

16

更新

2.3.0-rc.0开始支持组件继承

原版的

到目前为止,对我来说最方便的是将模板和样式保存在单独的*html*.css文件中,并通过templateUrl和进行指定styleUrls,因此很容易重用。

@Component {
    selector: 'my-panel',
    templateUrl: 'app/components/panel.html', 
    styleUrls: ['app/components/panel.css']
}
export class MyPanelComponent extends BasePanelComponent

2
这正是我所需要的。BaseComponent组件的@Component装饰器会是什么样?它可以引用不同的html / css文件吗?它可以引用MyPanelComponent引用的相同html / css文件吗?
ebhh2001 '16

1
这不继承@Input()@Output()装饰,不是吗?
Leon Adler

10

据我所知,Angular 2尚未实现组件继承,我不确定他们是否有计划,但是由于Angular 2使用的是Typescript(如果您决定采用这种方式),则可以使用类继承通过做class MyClass extends OtherClass { ... }。对于组件继承,我建议通过转到https://github.com/angular/angular/issues并提交功能请求来参与Angular 2项目!


知道了,我将在接下来的几天中尝试对angular2项目进行迭代,并确认请求功能不再存在于Git的项目问题中;否则,我将起草资源请求,因为在我看来这是非常有趣的特征。是否有任何其他参数可以提出最有趣的要求?
Fernando Leal

1
关于我在最初的解决方案(export class MyPanelComponent extends BasePanelComponent)中已经在使用的继承资源的打字稿,问题仅在于注释/装饰符未继承的情况下。
Fernando Leal

1
是的,我真的不知道您还能添加什么。我喜欢这样的想法:要么有一个@SubComponent()将类标记为子组件的新装饰器(类似),要么在装饰器上有一个额外的字段,该字段@Component允许您引用要从其继承的父组件。
watzon '16

1
功能请求angular2已添加到GitHub上的项目中:扩展/继承angular2组件注释#7968
Fernando Leal

9

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

该组件仅继承类逻辑:

  • @Component装饰器中的所有元数据都不会被继承。
  • 组件的@Input属性和@Output属性是继承的。
  • 组件生命周期不被继承。

要牢记这些功能非常重要,因此让我们独立检查每个功能。

组件仅继承类逻辑

当您继承Component时,内部的所有逻辑均被同等继承。值得注意的是,只有公共成员才能继承,因为私有成员只能在实现它们的类中访问。

@Component装饰器中的所有元数据都不继承

起初没有继承元数据的事实乍看起来似乎是违反直觉的,但是,如果您对此进行考虑,这实际上是很合理的。如果从Component(组件A)继承,则不希望从中继承ComponentA的选择器替换继承的类ComponentB的选择器。对于template / templateUrl以及style / styleUrls可以说相同。

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

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

组件生命周期未继承

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

调用超级/基础组件方法

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


5

如果您阅读CDK库和材料库,它们使用的是继承关系,但对于组件本身却使用的不是很多,那么内容投影就是IMO的王者。请参阅此链接https://blog.angular-university.io/angular-ng-content/,其中显示“此设计的关键问题”

我知道这不能回答您的问题,但我确实认为应该避免继承/扩展组件。这是我的理由:

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

如果抽象类包含共享变量或onClicketc函数,则两个扩展组件视图的html之间将出现重复。这是一种不好的做法,并且共享的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();
    });
  }
}

之所以如此,是因为在整个大型代码库infiniteSubscriptions.push()中仅使用了10次。导入和扩展AutoUnsubscribe实际上还需要花费更多的代码,而不仅仅是添加组件本身mySubscription.unsubscribe()ngOnDestroy()方法,总之,这需要附加的逻辑。


好的,我了解您的配置,并且我同意聚合几乎可以解决似乎需要继承的所有问题。将组件视为可以以多种方式对接的应用程序的一小部分总是很有趣的。但是在出现问题的情况下,问题是我无法控制/无法访问要继承的组件(它是第三个组件)中的修改,那么聚合将变得不可行,并且继承将是理想的解决方案。
费尔南多·里尔

您为什么不能仅制作一个封装该第三方组件的新组件?您感兴趣的第三方成分是什么?例如<my-calendar [stuff] = stuff> <third-party-calendar [stuff] = stuff> </ ..> </ ..>
罗伯特·金(Robert king)

@robertking重复自己的工作方式非常虚弱...这就是为什么您会开始讨厌工作..而不是喜欢它的原因。
Dariusz Filipiak '18

对我来说,扩展组件是一个好主意,以防您希望一组组件具有相同的输入/输出参数,因此它们可以表现为一个。例如,我有几个注册步骤(credentialsStep,addressStep,selectBenefitsStep)。它们都应具有相同的输入选项(stepName,actionButtons ...)和输出(完成,取消)。
Sergey_T

@Sergey_T您可以考虑使用ng select和内容投影​​的一个组件吗?同样,重复几次输入似乎并没有真正节省很多功能TBH。
罗伯特·金

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

强烈不鼓励定制装饰器吗?在许多其他帖子/主题中,此解决方案被标记为完全错误,因为AOT不支持它们?
TerNovi

2

您可以继承@ 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 {

}

1

可以像打字稿类继承一样扩展组件,只是必须用新名称覆盖选择器。父组件的所有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


您能举例说明(使用问题的例子)如何工作吗?您可以使用stackblitz开发示例并共享链接。
费尔南多·里尔

@Component是一个装饰器,装饰器在类声明期间应用,而不是在对象上。
马赫什(MAHESH VALIYA VEETIL)

你是对的。装饰者没有任何区别。仅在将基本组件用作其他组件
时才需要

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);
        }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.