angular - Cum să extinzi/să moștenești componentele?


Aș dori să creez extensii pentru unele componente deja implementate în Angular 2, fără a fi nevoie să le rescriu aproape complet, deoarece componenta de bază ar putea suferi modificări și aș dori ca aceste modificări să fie reflectate și în componentele sale derivate.

Am creat acest exemplu simplu pentru a încerca să îmi explic mai bine întrebările:

Cu următorul component de bază 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);
  }
}

Ați dori să creați un alt component derivat doar pentru a modifica, de exemplu, comportamentul unui component de bază în cazul culorii exemplu, 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";
  }
}

Exemplu complet funcțional în Plunker

Notă: Evident, acest exemplu este simplu și ar putea fi rezolvat altfel, nu este necesar să se folosească moștenirea, dar este destinat doar să ilustreze problema reală.

După cum se poate vedea în implementarea componentei derivate app/my-panel.component.ts, o mare parte din implementare a fost repetată, iar singura parte care a fost cu adevărat moștenită a fost class BasePanelComponent, dar @Component a trebuit practic să fie complet repetat, nu doar părțile modificate, cum ar fi selector: 'my-panel'.

Există vreo modalitate de a realiza o moștenire literală completă a unui component Angular2, moștenind definiția class a marcajelor/annotațiilor, de exemplu @Component?

Editare 1 - Cerere de Funcționalitate

Cerere de funcționalitate angular2 adăugată proiectului pe GitHub: Extindere/Moștenire a adnotărilor componentelor angular2 #7968

Editare 2 - Cerere Închisă

Cererea a fost închisă, din acest motiv, că pe scurt nu s-ar ști cum să se combine decoratorul care va fi realizat. Lăsându-ne fără opțiuni. Deci, părerea mea este citată în Issue.

Răspuns #1

Soluție alternativă:

Acest răspuns al lui Thierry Templier este o modalitate alternativă de a ocoli problema.

După câteva întrebări cu Thierry Templier, am ajuns la următorul exemplu funcțional care corespunde așteptărilor mele ca alternativă la limitarea moștenirii menționată în această întrebare:

1 - Creează un decorator personalizat:

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ă de bază cu decoratorul @Component:

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

}

3 - Subcomponent cu decoratori @CustomComponent:

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

Plunkr cu exemplu complet.

Răspuns #2

Versiunea 2.3 a Angular 2 tocmai a fost lansată și include moștenirea nativă a componentelor. Se pare că poți moșteni și suprascrie orice vrei, cu excepția șabloanelor și a stilurilor. Câteva referințe:

  • Funcții noi în Angular 2.3

  • Moștenirea componentelor în Angular 2

  • Un Plunkr care demonstrează moștenirea componentelor

Răspunsul #3

actualizare

Moștenirea componentelor este acceptată începând cu 2.3.0-rc.0

original

Până acum, cel mai convenabil pentru mine este să păstrez template-ul și stilurile în fișiere separate *html și *.css și să le specific prin templateUrl și styleUrls, astfel încât să fie ușor de reutilizat.

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

Răspunsul #4

Acum că TypeScript 2.2 acceptă Mixins prin expresii de clasă, avem o modalitate mult mai bună de a exprima Mixins pe componente. Țineți cont că puteți de asemenea să folosiți moștenirea componentelor începând cu Angular 2.3 (discuție) sau un decorator personalizat, așa cum s-a discutat în alte răspunsuri aici. Totuși, cred că Mixins au unele proprietăți care le fac de preferat pentru reutilizarea comportamentului între componente:

  • Mixinele se compun mai flexibil, adică poți să combini și potrivești mixinele pe componente existente sau să combini mixinele pentru a forma componente noi
  • Compoziția mixin rămâne ușor de înțeles datorită liniarizării sale evidente în ierarhia de moștenire a claselor
  • Poți evita mai ușor problemele cu decoratoarele și adnotările care afectează moștenirea componentelor (discuție)

Îți recomand cu tărie să citești anunțul TypeScript 2.2 de mai sus pentru a înțelege cum funcționează Mixins. Discuțiile legate în problemele GitHub ale Angular oferă detalii suplimentare.

Vei avea nevoie de aceste tipuri:

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

export class MixinRoot {
}

Și apoi poți declara un Mixin astfel Destroyable mixin care ajută componentele să țină evidența abonamentelor care trebuie eliminate în 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
    }
  };
}

Pentru a integra Destroyable într-un Component, îți declari componenta astfel:

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

Rețineți că MixinRoot este necesar doar atunci când doriți să extindeți o compoziție de Mixin. Puteți extinde cu ușurință mai multe mixins, de exemplu A extends B(C(D)). Aceasta este linearizarea evidentă a mixin-urilor despre care am vorbit mai sus, de exemplu, compuneți efectiv o ierarhie de moștenire A -> B -> C -> D.

În alte cazuri, de exemplu când doriți să compuneți Mixins pe o clasă existentă, puteți aplica Mixin-ul astfel:

const MyClassWithMixin = MyMixin(MyClass);

Totuși, am constatat că prima metodă funcționează cel mai bine pentru Components și Directives, deoarece acestea trebuie oricum decorate cu @Component sau @Directive.

Răspunsul #5

Din câte știu, moștenirea componentelor nu a fost implementată încă în Angular 2 și nu sunt sigur dacă au planuri în acest sens, însă, deoarece Angular 2 folosește TypeScript (dacă ai decis să mergi pe această cale), poți folosi moștenirea de clase făcând class MyClass extends OtherClass { ... }. Pentru moștenirea componentelor, aș sugera să te implici în proiectul Angular 2 mergând la https://github.com/angular/angular/issues și să trimiți o solicitare de funcționalitate!

Răspunsul #6

dacă parcurgi bibliotecile CDK și bibliotecile de material, ele folosesc moștenirea, dar nu atât de mult pentru componentele în sine, proiecția conținutului este rege, după părerea mea. vezi acest link https://blog.angular-university.io/angular-ng-content/ unde se spune „problema principală cu acest design”

Știu că acest lucru nu răspunde la întrebarea ta, dar \nCred cu adevărat că moștenirea/extinderea componentelor ar trebui evitată. Iată raționamentul meu:

Dacă clasa abstractă extinsă de două sau mai multe componente conține logică comună: folosește un serviciu sau chiar creează o nouă clasă TypeScript care poate fi împărtășită între cele două componente.

Dacă clasa abstractă... conține variabile partajate sau funcții precum onClick, atunci va exista duplicare între HTML-ul celor două componente care o extind. Aceasta este o practică proastă și acel HTML partajat trebuie să fie împărțit în Component(e). Aceste Component(e) (părți) pot fi partajate între cele două componente.

Îmi scapă alte motive pentru a avea o clasă abstractă pentru componente?

Un exemplu pe care l-am văzut recent a fost componente care extind 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();
    });
  }
}

acest lucru a fost rău deoarece de-a lungul unui cod mare, infiniteSubscriptions.push() a fost folosit doar de 10 ori. De asemenea, importarea și extinderea AutoUnsubscribe necesită de fapt mai mult cod decât simpla adăugare a mySubscription.unsubscribe() în metoda ngOnDestroy() a componentei, ceea ce oricum necesita logică suplimentară.