Como estender / herdar componentes?

160

Eu gostaria de criar extensões para alguns componentes já implantados no Angular 2, sem precisar reescrevê-los quase completamente, pois o componente base pode sofrer alterações e desejar que essas alterações também sejam refletidas em seus componentes derivados.

Criei este exemplo simples para tentar explicar melhor minhas perguntas:

Com o seguinte componente base 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);
  }
}

Deseja criar outro componente derivativo apenas para alterar, por exemplo, um comportamento básico do componente no caso da cor de exemplo 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";
  }
}

Exemplo completo de trabalho no Plunker

Nota: Obviamente, este exemplo é simples e pode ser resolvido caso contrário, não há necessidade de usar herança, mas destina-se apenas a ilustrar o problema real.

Como você pode ver na implementação do componente derivado app/my-panel.component.ts, grande parte da implementação foi repetida, e a única parte realmente herdada foi a class BasePanelComponent, mas @Componentteve que ser basicamente repetida completamente, não apenas as partes alteradas, como a selector: 'my-panel'.

Existe alguma maneira de fazer uma herança literalmente completa de um componente Angular2, herdando a classdefinição das marcações / anotações, como por exemplo @Component?

Editar 1 - Solicitação de recurso

Solicitação de recurso angular2 adicionado ao projeto no GitHub: Estender / herdar anotações de componentes angular2 # 7968

Editar 2 - Solicitação Encerrada

A solicitação foi encerrada, por esse motivo , que brevemente não saberia como mesclar o decorador. Deixando-nos sem opções. Portanto, minha opinião é citada na edição .

Fernando Leal
fonte
Verifique esta resposta stackoverflow.com/questions/36063627/… Atenciosamente
NicolasB
Ok NicolasB. Mas meu problema é com a herança do decorador @Component, que não é aplicado aos metadados da herança. = /
Fernando Leal
pessoas, evite usar herança com angular. por exemplo, a classe de exportação PlannedFilterComponent estende AbstractFilterComponent implementa OnInit {muito ruim. Existem outras maneiras de compartilhar código, por exemplo, serviços e componentes menores. A herança não é o caminho angular. Estou em um projeto angular em que eles usaram herança e há coisas que quebram, como exportar componentes que herdam de componentes abstratos sem entradas da classe abstrata.
robert king
1
usar a projeção de conteúdo em vez eg github.com/angular/components/blob/master/src/material/card/... não usar a herança
Robert King

Respostas:

39

Solução alternativa:

Esta resposta de Thierry Templier é uma maneira alternativa de contornar o problema.

Após algumas perguntas com Thierry Templier, cheguei ao seguinte exemplo de trabalho que atende às minhas expectativas como alternativa à limitação de herança mencionada nesta pergunta:

1 - Crie um decorador personalizado:

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 - Componente base com o decorador @Component:

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

}

3 - Subcomponente com o decorador @CustomComponent:

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

Plunkr com exemplo completo.

Fernando Leal
fonte
3
Presumo que isso não seja compatível com o compilador de modelos offline.
Günter Zöchbauer
@ GünterZöchbauer, não tenho conhecimento sobre "modelo de compilador offline" do Angular2. Mas acho que isso pode não ser compatível, e seria uma opção alternativa. Onde o modo "compilador de modelo offline" do Angular2 seria útil? Você pode me mostrar algo para entender melhor sobre isso? Para que eu possa entender a importância dessa compatibilidade para o meu projeto.
Fernando Leal
O compilador de modelos offline (OTC) ainda não está funcional, embora já esteja incluído no RC.3. OTC analisará os decoradores e gerará código durante uma etapa de construção quando o implementável for gerado. O OTC permite remover o analisador e o compilador Angular2 que processam decoradores e ligações em tempo de execução, o que leva a um tamanho de código menor notável e a uma inicialização mais rápida de aplicativos e componentes. OTC provavelmente se tornará utilizável com uma das próximas atualizações.
Günter Zöchbauer
1
@ GünterZöchbauer, agora entendo a importância de manter a funcionalidade compatível com o OTC. Será uma pré-compilação de decoradores angulares, reduzindo a sobrecarga para inicializar componentes. Eu gostaria de saber sobre o funcionamento desse processo e porque a solução desta resposta não será compatível com o OTC? Como é a pré-compilação de decoradores? Tendo esse conhecimento, podemos pensar em algo para manter essa alternativa funcional ao OTC. Obrigado pelo esclarecimento!
Fernando Leal
24

A versão 2.3 do Angular 2 foi lançada recentemente e inclui herança de componentes nativos. Parece que você pode herdar e substituir o que quiser, exceto modelos e estilos. Algumas referências:

Daniel Griscom
fonte
Uma "pegadinha" ocorre aqui quando você esquece de especificar um novo "seletor" no componente filho. Você receberá um erro de tempo de execução ao longo das linhas, More than one component matched on this elementcaso contrário.
O Aelfinn
@ TheAelfinn Sim: cada componente precisa ter uma especificação completa na @Component()tag. Mas, você pode compartilhar o .html ou .css consultando o mesmo arquivo, se desejar. Em suma, é uma grande vantagem.
Daniel Griscom
No seu segundo link , scotch.io/tutorials/component-inheritance-in-angular-2 , o autor afirma que os componentes herdam os serviços injetados de dependência de seus pais, meu código sugere o contrário. Você pode confirmar que isso é suportado?
O Aelfinn
18

Agora que o TypeScript 2.2 suporta Mixins através de expressões de classe , temos uma maneira muito melhor de expressar Mixins em componentes. Lembre-se de que você também pode usar a herança de componentes desde o angular 2.3 ( discussão ) ou um decorador personalizado, conforme discutido em outras respostas aqui. No entanto, acho que os Mixins têm algumas propriedades que os tornam preferíveis para reutilizar o comportamento entre os componentes:

  • Os mixins compõem de forma mais flexível, ou seja, você pode misturar e combinar mixins em componentes existentes ou combinar mixins para formar novos componentes
  • A composição da mixina permanece fácil de entender, graças à sua linearização óbvia a uma hierarquia de herança de classe
  • É mais fácil evitar problemas com decoradores e anotações que afetam a herança de componentes ( discussão )

Eu sugiro fortemente que você leia o anúncio do TypeScript 2.2 acima para entender como os Mixins funcionam. As discussões vinculadas nos problemas angulares do GitHub fornecem detalhes adicionais.

Você precisará destes tipos:

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

export class MixinRoot {
}

E então você pode declarar um Mixin como este Destroyablemixin que ajuda os componentes a acompanhar as assinaturas que precisam ser descartadas 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
    }
  };
}

Para misturar Destroyableem um Component, você declara seu componente assim:

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

Observe que isso MixinRootsó é necessário quando você deseja extenduma composição Mixin. Você pode estender facilmente vários mixins, por exemplo A extends B(C(D)). Esta é a linearização óbvia dos mixins que eu estava falando acima, por exemplo, você está efetivamente compondo uma hierarquia de herança A -> B -> C -> D.

Em outros casos, por exemplo, quando você deseja compor Mixins em uma classe existente, pode aplicar o Mixin da seguinte maneira:

const MyClassWithMixin = MyMixin(MyClass);

No entanto, achei a primeira maneira que funciona melhor para Componentse Directives, como também precisa ser decorada com @Componentou de @Directivequalquer maneira.

Johannes Rudolph
fonte
isso é incrível! Obrigado pela sugestão. MixinRoot está simplesmente sendo usado como um espaço reservado de classe vazio aqui? só quero ter certeza de que meu entendimento está correto.
Alex Lockwood
@AlexLockwood, sim, o espaço reservado vazio da classe é exatamente o que eu estou usando. Eu evitaria felizmente usá-lo, mas por enquanto não encontrei uma maneira melhor de fazê-lo.
Johannes Rudolph
2
Acabei usando function Destroyable<T extends Constructor<{}>>(Base = class { } as T). Dessa forma, eu posso criar mixins usando extends Destroyable().
Alex Lockwood
1
Parece muito bom, no entanto, parece que o AoT build (Cli1.3) remove o ngOnDestroy do DashBoardComponent, pois ele nunca é chamado. (mesmo vale para ngOnInit)
dzolnjan
obrigado por esta solução. No entanto, após um prod construir com cliques iônicos ou angulares, o mixin não funciona de alguma forma, como se não tivesse sido estendido.
Han Che
16

atualizar

A herança de componentes é suportada desde 2.3.0-rc.0

original

Até agora, o mais conveniente para mim é manter modelo e estilos em separado *htmle *.cssarquivos e especificar aqueles através dos templateUrle styleUrls, por isso é fácil reutilizável.

@Component {
    selector: 'my-panel',
    templateUrl: 'app/components/panel.html', 
    styleUrls: ['app/components/panel.css']
}
export class MyPanelComponent extends BasePanelComponent
am0wa
fonte
2
É exatamente disso que eu preciso. Como seria o decorador do @Component para o BasePanelComponent? Poderia referenciar diferentes arquivos html / css? Poderia referenciar os mesmos arquivos html / css referenciados por MyPanelComponent?
ebhh2001
1
Isso não herda @Input()e @Output()decoradores, não é?
Leon Adler
10

Até onde eu sei, a herança de componentes ainda não foi implementada no Angular 2 e não tenho certeza se eles têm planos, no entanto, como o Angular 2 está usando texto datilografado (se você decidiu seguir esse caminho), pode usar a herança de classe fazendo class MyClass extends OtherClass { ... }. Para herança de componentes, sugiro que você se envolva com o projeto Angular 2 acessando https://github.com/angular/angular/issues e enviando uma solicitação de recurso!

Watzon
fonte
Entendi, tentarei nos próximos dias iterar o projeto angular2 e verificar se o recurso de solicitação não está mais nos problemas do projeto no Git e, caso contrário, irei elaborar uma solicitação para o recurso, pois me parece muito interessante característica. Alguma idéia de argumentos extras para fazer a solicitação mais interessante?
Fernando Leal
1
Com relação ao texto datilografado do recurso de herança que já estou usando na minha solução inicial ( export class MyPanelComponent extends BasePanelComponent), o problema é apenas no caso de Anotações / Decoradores não serem herdados.
Fernando Leal
1
Sim, eu realmente não sei mais o que você poderia adicionar. Eu gosto da idéia de ter um novo decorador (algo como @SubComponent()) que marque uma classe como subcomponente ou ter um campo extra no @Componentdecorador que permita que você faça referência a um componente pai do qual herdar.
watzon
1
Solicitação de recurso angular2 adicionado ao projeto no GitHub: Estender / Herdar anotações de componentes angular2 # 7968
Fernando Leal
9

Vamos entender algumas das principais limitações e recursos do sistema de herança de componentes da Angular.

O componente herda apenas a lógica da classe:

  • Todos os metadados no decorador @Component não são herdados.
  • As propriedades @Input do componente e as propriedades @Output são herdadas.
  • O ciclo de vida do componente não é herdado.

Esses recursos são muito importantes para ter em mente, portanto, vamos examinar cada um de forma independente.

O componente herda apenas a lógica da classe

Quando você herda um componente, toda a lógica interna é igualmente herdada. Vale a pena notar que apenas membros públicos são herdados, pois membros privados são acessíveis apenas na classe que os implementa.

Todos os metadados no decorador @Component não são herdados

O fato de nenhum metadado ser herdado pode parecer contra-intuitivo no começo, mas, se você pensar sobre isso, faz todo o sentido. Se você herdar de um Component digamos (componentA), não desejaria que o seletor de ComponentA, que você está herdando, substitua o seletor de ComponentB, que é a classe que está herdando. O mesmo pode ser dito para o template / templateUrl, bem como para o style / styleUrls.

As propriedades @Input e @Output do componente são herdadas

Esse é outro recurso que eu realmente amo sobre a herança de componentes no Angular. Em uma frase simples, sempre que você tiver uma propriedade personalizada @Input e @Output, essas propriedades serão herdadas.

O ciclo de vida do componente não é herdado

Essa parte não é tão óbvia, especialmente para pessoas que não trabalharam extensivamente com os princípios de POO. Por exemplo, digamos que você tenha o ComponentA, que implementa um dos muitos ganchos do ciclo de vida do Angular, como o OnInit. Se você criar o ComponentB e herdar o ComponentA, o ciclo de vida do OnInit do ComponentA não será acionado até você explicitamente chamá-lo, mesmo se você tiver esse ciclo de vida do OnInit para o ComponentB.

Chamando métodos de componente super / básico

Para ter o método ngOnInit () do componente ComponentA, precisamos usar a super palavra-chave e, em seguida, chamar o método que precisamos, que neste caso é ngOnInit. A super palavra-chave refere-se à instância do componente que está sendo herdado da qual, neste caso, será ComponentA.

Masoud Bimar
fonte
5

se você ler as bibliotecas CDK e as bibliotecas de materiais, elas estão usando herança, mas não tanto para os componentes, a projeção de conteúdo é o principal IMO. veja este link https://blog.angular-university.io/angular-ng-content/ onde diz "o principal problema com esse design"

Sei que isso não responde à sua pergunta, mas realmente acho que a herança / extensão de componentes deve ser evitada . Aqui está o meu raciocínio:

Se a classe abstrata estendida por dois ou mais componentes contiver lógica compartilhada: use um serviço ou crie uma nova classe de texto datilografada que possa ser compartilhada entre os dois componentes.

Se a classe abstrata ... contiver variáveis ​​compartilhadas ou funções onClicketc, haverá duplicação entre o html das duas visualizações de componentes estendidos. Esta é uma prática ruim e o html compartilhado precisa ser dividido em componentes. Esses componentes (peças) podem ser compartilhados entre os dois componentes.

Estou perdendo outros motivos para ter uma classe abstrata para componentes?

Um exemplo que vi recentemente foram os componentes que estendem o 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();
    });
  }
}

isso foi bas porque em uma grande base de código, infiniteSubscriptions.push()foi usado apenas 10 vezes. Também importar e estender, AutoUnsubscribena verdade, requer mais código do que apenas adicionar mySubscription.unsubscribe()o ngOnDestroy()método do próprio componente, o que exigia lógica adicional de qualquer maneira.

Robert King
fonte
Ok, entendo sua colocação e concordo que a agregação quase resolve todos os problemas que parecem precisar de herança. E é sempre interessante pensar nos componentes como pequenas partes do aplicativo que podem ser acopladas de várias maneiras. Mas, no caso da questão, o problema é que não tenho controle / acesso a modificações no componente que quero herdar (é um terceiro componente), a agregação se tornaria inviável e a herança seria a solução ideal.
Fernando Leal
por que você não pode simplesmente criar um novo componente que encapsule esse componente de terceiros? Qual é o seu componente de terceiro interesse? eg <my-calendário [coisas] = coisas> <-terceiro-calendário [coisas] = coisas> </ ..> </ ..>
Robert King
@robertking se repetir é um padrão muito fraco ... É por isso que você começa a odiar o seu trabalho ... em vez de se divertir.
Dariusz Filipiak 26/03
Quanto a mim, é uma boa idéia estender os componentes, caso você queira ter os mesmos parâmetros de entrada / saída para um conjunto de componentes, para que eles possam se comportar como um. Por exemplo, tenho várias etapas de registro (credentialsStep, addressStep, selectBenefitsStep). Todos eles devem ter as mesmas opções de Entrada (stepName, actionButtons ...) e Saídas (concluir, cancelar).
Sergey_T 5/09/19
@Sergey_T você poderia considerar um componente com ng seleção e projeção de conteúdo? Também repetir algumas entradas não parece que você está realmente economizando muita funcionalidade TBH.
robert king
2

Se alguém está procurando uma solução atualizada, a resposta de Fernando é praticamente perfeita. Exceto que ComponentMetadatafoi preterido. Usando Componentvez trabalhou para mim.

O CustomDecorator.tsarquivo completo do Decorador personalizado é assim:

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

Em seguida, importe-o para o seu novo sub-component.component.tsarquivo de componente e use em @CustomComponentvez de @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!
}
Rhyneav
fonte
Os decoradores personalizados não são altamente desencorajados? De muitos outros posts / threads, esta solução foi marcada como completamente errada, porque a AOT não os suporta?
TerNovi 25/05
2

Você pode herdar @Input, @Output, @ViewChild, etc. Veja o exemplo:

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

}
Mizuwokiru
fonte
1

Os componentes podem ser estendidos da mesma forma que uma herança de classe datilografada, apenas para substituir o seletor por um novo nome. Todas as propriedades de entrada () e saída () do componente pai funcionam normalmente

Atualizar

@Component é um decorador,

Decoradores são aplicados durante a declaração de classe e não em objetos.

Basicamente, os decoradores adicionam alguns metadados ao objeto de classe e não podem ser acessados ​​por herança.

Se você deseja obter a herança do decorador, sugiro escrever um decorador personalizado. Algo como o exemplo abaixo.

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

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

MAHESH VALIYA VEETIL
fonte
Você poderia exemplificar (usando o exemplo da pergunta) como isso funcionaria? Você pode usar o stackblitz para desenvolver o exemplo e compartilhar o link.
Fernando Leal
O @Component é um decorador, os decoradores são aplicados durante a declaração da classe que não está nos objetos.
MAHESH VALIYA VEETIL
Você está certo. Decoradores não fazem diferença. É necessário apenas se o componente base for usado como um componente em outro lugar
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);
        }
}
Mittal Bhatt
fonte