¿Cómo extender / heredar componentes?

160

Me gustaría crear extensiones para algunos componentes ya implementados en Angular 2, sin tener que reescribirlos casi por completo, ya que el componente base podría sufrir cambios y desear que estos cambios también se reflejen en sus componentes derivados.

Creé este ejemplo simple para tratar de explicar mejor mis preguntas:

Con el siguiente 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);
  }
}

¿Desea crear otro componente derivado solo alterar, por ejemplo, un comportamiento de componente básico en el caso del color de ejemplo 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";
  }
}

Ejemplo de trabajo completo en Plunker

Nota: Obviamente, este ejemplo es simple y podría resolverse; de ​​lo contrario, no es necesario utilizar la herencia, pero solo tiene la intención de ilustrar el problema real.

Como puede ver en la implementación del componente derivado app/my-panel.component.ts, gran parte de la implementación se repitió, y la única parte realmente heredada fue la class BasePanelComponent, pero @Componentbásicamente tuvo que repetirse por completo, no solo las porciones cambiadas, como la selector: 'my-panel'.

¿Hay alguna manera de hacer una herencia literalmente completa de un componente Angular2, heredando la classdefinición de las marcas / anotaciones, como por ejemplo @Component?

Edición 1 - Solicitud de funciones

Solicitud de función angular2 agregada al proyecto en GitHub: Extender / Heredar anotaciones de componentes angular2 # 7968

Edición 2 - Solicitud cerrada

La solicitud se cerró, por este motivo , que brevemente no sabría cómo fusionar el decorador se realizará. Dejándonos sin opciones. Así que mi opinión se cita en el número .

Fernando Leal
fuente
Comprueba esta respuesta stackoverflow.com/questions/36063627/… Saludos
NicolasB
Ok NicolasB Pero mi problema es con la herencia del decorador @Component, que no se aplica a los metadatos de herencia. = /
Fernando Leal
personas, por favor eviten usar herencia con angular. por ejemplo, la clase de exportación PlannedFilterComponent extiende AbstractFilterComponent implementa OnInit {es muy malo. Hay otras formas de compartir código, por ejemplo, servicios y componentes más pequeños. La herencia no es la forma angular. Estoy en un proyecto angular donde usaron herencia y hay cosas que se rompen, como exportar componentes que heredan de componentes abstractos que faltan entradas de la clase abstracta.
Robert King
1
use la proyección de contenido, por ejemplo, github.com/angular/components/blob/master/src/material/card/… no use la herencia
robert king

Respuestas:

39

Solución alternativa:

Esta respuesta de Thierry Templier es una forma alternativa de solucionar el problema.

Después de algunas preguntas con Thierry Templier, llegué al siguiente ejemplo de trabajo que cumple con mis expectativas como una alternativa a la limitación de herencia mencionada en esta pregunta:

1 - Crear 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 con decorador @Component:

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

}

3 - Subcomponente con el decorador @CustomComponent:

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

Plunkr con ejemplo completo.

Fernando Leal
fuente
3
Supongo que esto no será compatible con el compilador de plantillas sin conexión.
Günter Zöchbauer
@ GünterZöchbauer, no tengo conocimiento acerca de la "plantilla del compilador fuera de línea" de Angular2. Pero creo que eso puede no ser compatible, y sería una opción alternativa. ¿Dónde sería útil el modo "compilador de plantillas fuera de línea" de Angular2? ¿Me puede mostrar algo para entender mejor sobre esto? Entonces puedo entender la importancia de esta compatibilidad para mi proyecto.
Fernando Leal
El compilador de plantillas sin conexión (OTC) aún no funciona aunque ya está incluido en RC.3. OTC analizará los decoradores y generará código durante un paso de compilación cuando se genere la implementación. OTC permite eliminar el analizador y compilador Angular2 que procesa decoradores y enlaces en tiempo de ejecución, lo que conduce a un notable menor tamaño de código y una inicialización más rápida de la aplicación y los componentes. OTC probablemente se podrá usar con una de las próximas actualizaciones.
Günter Zöchbauer
1
@ GünterZöchbauer, ahora entiendo la importancia de mantener la funcionalidad compatible con OTC. Será una precompilación de decoradores angulares que reducirá la sobrecarga para inicializar los componentes. Me gustaría saber sobre el funcionamiento de este proceso y porque la solución de esta respuesta no será compatible con OTC. ¿Cómo es la precompilación de decoradores? Teniendo este conocimiento, podríamos pensar en algo para mantener esta alternativa funcional a OTC. ¡Gracias por la aclaración!
Fernando Leal
24

Angular 2 versión 2.3 se acaba de lanzar, e incluye herencia de componentes nativos. Parece que puedes heredar y anular lo que quieras, excepto plantillas y estilos. Algunas referencias:

Daniel Griscom
fuente
Aquí se produce un "problema" cuando olvida especificar un nuevo "selector" en el componente secundario. Obtendrá un error de tiempo de ejecución en la línea de More than one component matched on this elementsi no lo hace.
El Aelfinn
@TheAelfinn Sí: cada componente debe tener una especificación completa en la @Component()etiqueta. Pero, si lo desea, puede compartir .html o .css haciendo referencia al mismo archivo. En general, es una gran ventaja.
Daniel Griscom el
En su segundo enlace scotch.io/tutorials/component-inheritance-in-angular-2 , el autor afirma que los componentes heredan los servicios inyectados de dependencia de sus padres, mi código sugiere lo contrario. ¿Puedes confirmar que esto es compatible?
El Aelfinn
18

Ahora que TypeScript 2.2 admite Mixins a través de expresiones de clase , tenemos una forma mucho mejor de expresar Mixins en componentes. Tenga en cuenta que también puede usar la herencia de componentes desde angular 2.3 ( discusión ) o un decorador personalizado como se describe en otras respuestas aquí. Sin embargo, creo que los Mixins tienen algunas propiedades que los hacen preferibles para reutilizar el comportamiento entre los componentes:

  • Los Mixins componen de manera más flexible, es decir, puede mezclar y combinar Mixins en componentes existentes o combinar Mixins para formar nuevos Componentes
  • La composición de Mixin sigue siendo fácil de entender gracias a su linealización obvia a una jerarquía de herencia de clase
  • Puede evitar más fácilmente problemas con decoradores y anotaciones que afectan la herencia de componentes ( discusión )

Le sugiero que lea el anuncio TypeScript 2.2 anterior para comprender cómo funcionan los Mixins. Las discusiones vinculadas en cuestiones angulares de GitHub proporcionan detalles adicionales.

Necesitarás estos tipos:

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

export class MixinRoot {
}

Y luego puede declarar un Mixin como este Destroyablemixin que ayuda a los componentes a realizar un seguimiento de las suscripciones que deben eliminarse 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 mezclar Destroyableen un Component, declaras tu componente así:

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

Tenga en cuenta que MixinRootsolo es necesario cuando desea extenduna composición Mixin. Puede extender fácilmente múltiples mixins, por ejemplo A extends B(C(D)). Esta es la linealización obvia de los mixins de los que hablaba anteriormente, por ejemplo, está componiendo efectivamente una jerarquía de herencia A -> B -> C -> D.

En otros casos, por ejemplo, cuando desea componer Mixins en una clase existente, puede aplicar Mixin de la siguiente manera:

const MyClassWithMixin = MyMixin(MyClass);

Sin embargo, descubrí que la primera forma funciona mejor Componentsy Directives, ya que también deben decorarse con @Componento de @Directivetodos modos.

Johannes Rudolph
fuente
esto es asombroso! Gracias por la sugerencia. ¿Se está utilizando MixinRoot simplemente como un marcador de posición de clase vacío aquí? Solo quiero asegurarme de que mi comprensión es correcta.
Alex Lockwood
@AlexLockwood, sí, el marcador de posición de clase vacío es exactamente para lo que lo estoy usando. Felizmente evitaría usarlo, pero por ahora no he encontrado una mejor manera de hacerlo.
Johannes Rudolph
2
Terminé usando function Destroyable<T extends Constructor<{}>>(Base = class { } as T). De esa manera puedo crear mixins usando extends Destroyable().
Alex Lockwood
1
Esto se ve muy bien, sin embargo, parece que la compilación de AoT (Cli1.3) elimina ngOnDestroy de DashBoardComponent ya que nunca se llama. (Lo mismo ocurre con ngOnInit)
dzolnjan
Gracias por esta solución. Sin embargo, después de una construcción de productos con iónico o angular-cli, el mixin no funciona de alguna manera, como si no se hubiera extendido.
Han Che
16

actualizar

La herencia de componentes es compatible desde 2.3.0-rc.0

original

Hasta el momento, el más conveniente para mí es mantener la plantilla y estilos en separada *htmly *.cssarchivos y especificar aquellos a través de templateUrly styleUrls, lo que es fácil reutilizable.

@Component {
    selector: 'my-panel',
    templateUrl: 'app/components/panel.html', 
    styleUrls: ['app/components/panel.css']
}
export class MyPanelComponent extends BasePanelComponent
am0wa
fuente
2
Esto es exactamente lo que necesito. ¿Cómo sería el decorador @Component para BasePanelComponent? ¿Podría hacer referencia a diferentes archivos html / css? ¿Podría hacer referencia a los mismos archivos html / css a los que hace referencia MyPanelComponent?
ebhh2001
1
Esto no hereda @Input()y @Output()decoradores, ¿verdad?
Leon Adler
10

Hasta donde sé, la herencia de componentes aún no se ha implementado en Angular 2 y no estoy seguro de si tienen planes, sin embargo, dado que Angular 2 está usando el mecanografiado (si ha decidido seguir esa ruta), puede usar la herencia de clase al hacerlo class MyClass extends OtherClass { ... }. Para la herencia de componentes, sugeriría involucrarse con el proyecto Angular 2 yendo a https://github.com/angular/angular/issues y enviando una solicitud de función.

Watzon
fuente
Lo tengo, lo intentaré en los próximos días repitiéndome el proyecto angular2 y verificaré que la función de solicitud ya no esté en los problemas del proyecto en Git y si no, redactaré una solicitud para el recurso, ya que me parece muy interesante característica. ¿Alguna idea de argumentos adicionales para hacer la solicitud más interesante?
Fernando Leal
1
Con respecto al mecanografiado del recurso de herencia que ya estoy usando en mi solución inicial ( export class MyPanelComponent extends BasePanelComponent), el problema es solo en el caso de que las Anotaciones / Decoradores no se hereden.
Fernando Leal
1
Sí, realmente no sé qué más podrías agregar. Me gusta la idea de tener un nuevo decorador (algo así @SubComponent()) que marque una clase como un subcomponente o tener un campo adicional en el @Componentdecorador que le permita hacer referencia a un componente principal del que heredar.
watzon
1
Solicitud de función angular2 agregada al proyecto en GitHub: Extender / Heredar anotaciones de componentes angular2 # 7968
Fernando Leal
9

Comprendamos algunas limitaciones y características clave en el sistema de herencia de componentes de Angular.

El componente solo hereda la lógica de clase:

  • Todos los metadatos en el decorador @Component no se heredan.
  • Las propiedades de componente @Input y @Output se heredan.
  • El ciclo de vida de los componentes no se hereda.

Es muy importante tener en cuenta estas características, así que examinemos cada una de forma independiente.

El componente solo hereda la lógica de clase

Cuando hereda un Componente, toda la lógica interna es igualmente heredada. Vale la pena señalar que solo los miembros públicos se heredan ya que los miembros privados solo son accesibles en la clase que los implementa.

Todos los metadatos en el decorador @Component no se heredan

El hecho de que no se hereden metadatos puede parecer contradictorio al principio, pero, si piensas en esto, tiene mucho sentido. Si hereda de un Componente, digamos (componenteA), no querrá que el selector de ComponenteA, del que está heredando, reemplace el selector de ComponenteB, que es la clase que está heredando. Lo mismo puede decirse de template / templateUrl y de style / styleUrls.

Las propiedades del componente @Input y @Output se heredan

Esta es otra característica que realmente me encanta de la herencia de componentes en Angular. En una oración simple, siempre que tenga una propiedad personalizada @Input y @Output, estas propiedades se heredan.

El ciclo de vida de los componentes no se hereda

Esta parte es la que no es tan obvia especialmente para las personas que no han trabajado extensamente con los principios de la POO. Por ejemplo, supongamos que tiene ComponentA que implementa uno de los muchos ganchos de ciclo de vida de Angular como OnInit. Si crea ComponentB y hereda ComponentA, el ciclo de vida de OnInit de ComponentA no se activará hasta que lo llame explícitamente, incluso si tiene este ciclo de vida de OnInit para ComponentB.

Llamar a métodos de componentes Super / Base

Para tener el método ngOnInit () de ComponentA fire, necesitamos usar la palabra clave super y luego llamar al método que necesitamos que en este caso es ngOnInit. La palabra clave súper se refiere a la instancia del componente que se hereda del cual en este caso será ComponentA.

Masoud Bimar
fuente
5

si lees las bibliotecas de CDK y las bibliotecas de materiales, están usando la herencia pero no tanto para los componentes en sí, la proyección de contenido es la OMI real. vea este enlace https://blog.angular-university.io/angular-ng-content/ donde dice "el problema clave con este diseño"

Sé que esto no responde a su pregunta, pero realmente creo que se debe evitar heredar / extender componentes . Aquí está mi razonamiento:

Si la clase abstracta extendida por dos o más componentes contiene lógica compartida: use un servicio o incluso cree una nueva clase de mecanografiado que pueda compartirse entre los dos componentes.

Si la clase abstracta ... contiene variables compartidas o funciones onClicketc, entonces habrá duplicación entre el html de las dos vistas de componentes extendidos. Esta es una mala práctica y que html compartido debe dividirse en Componente (s). Estos componentes (partes) se pueden compartir entre los dos componentes.

¿Me faltan otras razones para tener una clase abstracta para componentes?

Un ejemplo que vi recientemente fue componentes que extienden 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();
    });
  }
}

esto era bas porque en una base de código grande infiniteSubscriptions.push()solo se usaba 10 veces. Además, la importación y la extensión en AutoUnsubscriberealidad requieren más código que simplemente agregar mySubscription.unsubscribe()el ngOnDestroy()método del componente en sí, que de todos modos requería lógica adicional.

robert king
fuente
Ok, entiendo su colocación y estoy de acuerdo en que la agregación casi resuelve todos los problemas que parecen necesitar herencia. Y siempre es interesante pensar en los componentes como pequeñas partes de la aplicación que se pueden acoplar de varias maneras. Pero en el caso de la pregunta, el problema es que no tengo control / acceso a las modificaciones en el componente que quiero heredar (es un tercer componente), entonces la agregación sería inviable y la herencia sería la solución ideal.
Fernando Leal
¿por qué no puedes crear un nuevo componente que encapsule ese componente de terceros? ¿Cuál es su componente de terceros fuera de interés? por ejemplo, <my-calendar [stuff] = stuff> <third-party-calendar [stuff] = stuff> </ ..> </ ..>
robert king
@robertking repitiéndose es un patrón muy débil ... Es por eso que comenzarás a odiar tu trabajo ... en lugar de disfrutarlo.
Dariusz Filipiak
En cuanto a mí, es una buena idea extender componentes en caso de que desee tener los mismos parámetros de entrada / salida para un conjunto de componentes, para que puedan comportarse como uno solo. Por ejemplo, tengo varios pasos de registro (credentialsStep, addressStep, selectBenefitsStep). Todos deberían tener las mismas opciones de entrada (stepName, actionButtons ...) y salidas (completar, cancelar).
Sergey_T
@Sergey_T ¿podría considerar un componente con ng select y proyección de contenido? Además, repetir algunas entradas no parece que realmente esté ahorrando mucha funcionalidad TBH.
Robert King
2

Si alguien está buscando una solución actualizada, la respuesta de Fernando es bastante perfecta. Excepto que ComponentMetadataha quedado en desuso. Usar en Componentcambio funcionó para mí.

El CustomDecorator.tsarchivo completo de Custom Decorator se ve así:

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

Luego impórtelo a su nuevo sub-component.component.tsarchivo componente y use en @CustomComponentlugar de @Componentesto:

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
fuente
¿No se desalientan los decoradores personalizados? ¿De muchas otras publicaciones / hilos esta solución se ha marcado como completamente incorrecta porque AOT no los admitirá?
TerNovi
2

Puede heredar @Input, @Output, @ViewChild, etc. Mire la muestra:

@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
fuente
1

Los componentes se pueden extender igual que una herencia de clase mecanografiada, solo que tiene que anular el selector con un nuevo nombre. Todas las propiedades de entrada () y salida () del componente principal funcionan normalmente

Actualizar

@Component es un decorador,

Los decoradores se aplican durante la declaración de clase no en los objetos.

Básicamente, los decoradores agregan algunos metadatos al objeto de clase y no se puede acceder a ellos por herencia.

Si desea lograr la herencia del decorador, le sugiero que escriba un decorador personalizado. Algo como el siguiente ejemplo.

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
fuente
¿Podría ejemplificar (usando el ejemplo de la pregunta) cómo funcionaría? Puede usar stackblitz para desarrollar el ejemplo y compartir el enlace.
Fernando Leal
@Component es un decorador, los decoradores se aplican durante la declaración de clase no en los objetos.
MAHESH VALIYA VEETIL
Tienes razón. Los decoradores no hacen ninguna diferencia. Solo se requiere si el componente base se usa como componente en otro 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
fuente