Як розширити / успадкувати компоненти?


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 - Запит на функцію

Запит на особливості angular2 доданий до проекту на GitHub: Анотації компонентів Extend / Inherit angular2 # 7968

Редагувати 2 - Закритий запит

З цієї причини запит було закрито, щоб коротко не знати, як злити декоратор. Залишаючи нас без варіантів. Тож моя думка цитується у Випуску .


Перевірте цей відповідь stackoverflow.com/questions/36063627 / ... З повагою
NicolasB

Гаразд НіколасБ. Але моя проблема полягає у спадщині декоратора @Component, який не застосовується до метаданих про успадкування. = /
Фернандо Ліал

люди, будь ласка, уникайте використання спадщини з кутовими. наприклад, експортний клас PlannedFilterComponent розширює AbstractFilterComponent реалізує OnInit {дуже погано. Є й інші способи спільного використання коду, наприклад, сервіси та менші компоненти. Спадкування - це не кутовий шлях. Я перебуваю на кутовому проекті, де вони використовували успадкування, і є такі речі, які порушуються, наприклад, експорт компонентів, які успадковують з абстрактних компонентів відсутні вхідні дані абстрактного класу.
Роберт Кінг

1
замість цього використовуйте проекцію вмісту, наприклад, github.com/angular/components/blob/master/src/material/card/… не використовуйте спадщину
Роберт Кінг

Відповіді:


39

Альтернативне рішення:

Ця відповідь 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
Я припускаю, що це не буде сумісно з автономним компілятором шаблонів.
Günter Zöchbauer

@ GünterZöchbauer, я не знаю про "шаблон автономного компілятора" від Angular2. Але я думаю, що це може бути не сумісним, і це був би альтернативний варіант. Де був би корисний режим "офлайн-компілятор шаблонів" Angular2? Ви можете мені показати щось, щоб краще зрозуміти це? Тож я можу зрозуміти важливість цієї сумісності для мого проекту.
Фернандо Ліал

Офлайн-компілятор шаблонів (OTC) ще не функціонує, хоча він уже включений до RC.3. OTC буде аналізувати декоратори та генерувати код під час кроку збирання, коли генерується розгортається. OTC дозволяє видалити аналізатор і компілятор Angular2, які обробляють декоратори та прив’язки під час виконання, що призводить до помітного меншого розміру коду та більш швидкої ініціалізації додатків та компонентів. OTC, ймовірно, стане доступним для використання з одним із наступних оновлень.
Гюнтер Зехбауер

1
@ GünterZöchbauer, тепер я розумію важливість підтримки функціональності, сумісного з OTC. Це буде попередня збірка кутових декораторів, що зменшують накладні витрати для ініціалізації компонентів. Я хотів би знати про функціонування цього процесу і тому, що рішення цієї відповіді не буде сумісним з позабіржовим? Як здійснюється попередня збірка декораторів? Маючи ці знання, ми можемо придумати щось, щоб зберегти цю функціональну альтернативу OTC. Дякую за роз’яснення!
Фернандо Ліал

24

Angular 2 версії 2.3 щойно випущений, і він включає спадкування нативних компонентів. Схоже, ви можете успадкувати та змінити все, що завгодно, крім шаблонів та стилів. Деякі посилання:


Один "gotcha" тут виникає, коли ви забудете вказати новий "селектор" у дочірньому компоненті. More than one component matched on this elementЯкщо ви цього не зробите, ви отримаєте помилку виконання .
The Aelfinn

@TheAelfinn Так: кожен компонент повинен мати повну специфікацію в @Component()тезі. Але ви можете поділитися .html або .css, посилаючись на той самий файл, якщо хочете. Загалом, це великий плюс.
Даніель Гриском

У вашому другому посиланні scotch.io/tutorials/component-inheritance-in-angular-2 , автор стверджує, що компоненти успадковують послуги, що вводяться залежними від батьків, мій код пропонує інше. Чи можете ви підтвердити, що це підтримується?
The Aelfinn

18

Тепер, коли TypeScript 2.2 підтримує Mixins через вирази класу, у нас є набагато кращий спосіб виразити Mixins на компонентах. Зауважте, що ви також можете використовувати успадкування компонентів, починаючи з кутового 2.3 ( обговорення ) або спеціальний декоратор, як обговорюється в інших відповідях тут. Однак я думаю, що Mixins має деякі властивості, які роблять їх кращими для повторного використання поведінки в різних компонентах:

  • Mixins створює більш гнучко, тобто ви можете змішувати Mixin та поєднувати їх із наявними компонентами або комбінувати Mixins, щоб утворювати нові компоненти
  • Композиція Mixin залишається легко зрозумілою завдяки її очевидній лінеаризації до ієрархії спадкування класів
  • Ви можете легше уникнути проблем із декораторами та примітками, які спричиняють успадкування компонентів ( обговорення )

Я настійно пропоную прочитати вищезгадане повідомлення TypeScript 2.2, щоб зрозуміти, як працює Mixins. Пов’язані дискусії в кутових питаннях GitHub надають додаткову інформацію.

Вам знадобляться такі типи:

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

export class MixinRoot {
}

І тоді ви можете оголосити Mixin, як цей Destroyableміксин, який допомагає компонентам відстежувати підписки, які потрібно розмістити в 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. Ви можете легко розширити декілька міксинів, наприклад A extends B(C(D)). Це явна лінеаризація міксінів, про яку я говорив вище, наприклад, ви ефективно складете ієрархію спадкової спадщини A -> B -> C -> D.

В інших випадках, наприклад, коли ви хочете скласти Mixins на існуючому класі, ви можете застосувати Mixin так:

const MyClassWithMixin = MyMixin(MyClass);

Однак я знайшов, що перший спосіб працює найкраще Componentsі Directives, оскільки їх також потрібно прикрасити @Componentчи так чи @Directiveінакше.


Це круто! дякую за пропозицію. тут MixinRoot просто використовується як порожній заповнювач класу? просто хочу переконатися, що моє розуміння правильне.
Алекс Локвуд

@AlexLockwood yup, порожній заповнювач класу - це саме те, для чого я його використовую. Я б із задоволенням уникав його використовувати, але поки не знайшов кращого способу це зробити.
Йоганнес Рудольф

2
Я закінчив використовувати function Destroyable<T extends Constructor<{}>>(Base = class { } as T). Таким чином я можу створювати міксини, використовуючи extends Destroyable().
Алекс Локвуд

1
Це виглядає дуже добре, однак, схоже, що збірка AoT (Cli1.3) видаляє ngOnDestroy з DashBoardComponent, оскільки він ніколи не викликається. (те саме стосується ngOnInit)
dzolnjan

дякую за це рішення. Однак після складання prod з іонним або кутовим клі, міксин не працює якось, як ніби його не розширюють.
Хан Че

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
Це саме те, що мені потрібно. Як виглядатиме декоратор @Component для BasePanelComponent? Чи може він посилатися на різні файли html / css? Чи може він посилатися на ті самі файли html / css, на які посилається MyPanelComponent?
ebhh2001

1
Це не успадковує @Input()і @Output()декораторів, чи не так?
Леон Адлер

10

Наскільки я знаю, успадкування компонентів ще не реалізовано в Angular 2, і я не впевнений, чи планують вони, але оскільки Angular 2 використовує typecript (якщо ви вирішили пройти цей шлях), ви можете використовувати успадкування класів роблячи class MyClass extends OtherClass { ... }. Для успадкування компонентів я б запропонував брати участь у проекті Angular 2, перейшовши на сторінку https://github.com/angular/angular/isissue та подавши запит на функцію!


Зрозумів, я спробую в наступні дні повторити проект angular2 і переконатись, що функція запиту відсутня в проблемах проекту в Git, і якщо ні, то я складу запит на ресурс, оскільки мені це здається дуже цікавим особливість. Якась додаткова аргументація ідея зробити найбільш цікавий запит?
Фернандо Ліал

1
Щодо машинопису ресурсу спадкування, який я вже використовую в своєму початковому рішенні ( export class MyPanelComponent extends BasePanelComponent), проблема полягає лише в тому випадку, коли Анотації / Декоратори не передаються у спадок.
Фернандо Ліал

1
Так, я справді не знаю, що ще можна додати. Мені подобається ідея або мати новий декоратор (щось подібне @SubComponent()), який позначає клас як підкомпонент, або мати додаткове поле на @Componentдекораторі, що дозволяє посилатися на батьківський компонент, який слід успадкувати.
ватзон

1
Запит на функцію angular2 доданий до проекту на GitHub: Анотації
Fernando Leal

9

Давайте розберемося з деякими ключовими обмеженнями та особливостями системи успадкування компонентів Angular.

Компонент успадковує лише логіку класу:

  • Усі метадані в декораторі @Component не успадковуються.
  • Властивості компонента @ Input та @Output передаються у спадок.
  • Життєвий цикл компонента не успадковується.

Ці особливості дуже важливо мати на увазі, тому давайте розглянемо кожну окремо.

Компонент успадковує лише логіку класу

Коли ви успадковуєте Компонент, вся логіка всередині однаково успадковується. Варто зазначити, що успадковуються лише публічні члени, оскільки приватні члени доступні лише в класі, який їх реалізує.

Усі метадані в декораторі @Component не успадковуються

Той факт, що жодні метадані не успадковуються, може здатися спочатку інтуїтивно зрозумілим, але, якщо ви задумаєтесь про це, це насправді має сенс. Якщо ви успадковуєте з Component say (компонентA), ви не хочете, щоб селектор ComponentA, від якого ви успадковуєтесь, замінював селектор ComponentB, який є класом, який успадковує. Те саме можна сказати як для шаблону / templateUrl, так і для style / styleUrls.

Властивості компонентів @ Input та @Output передаються у спадок

Це ще одна особливість, яку мені дуже подобається за складовою «Спадництво в кутовій». У простому реченні, коли у вас є власні властивості @ Input та @Output, ці властивості передаються у спадок.

Життєвий цикл компонента не успадковується

Ця частина є не такою очевидною, особливо для людей, які не широко працювали з принципами ООП. Наприклад, скажімо, що у вас є ComponentA, який реалізує один із численних гаків життєвого циклу Angular, як OnInit. Якщо ви створюєте ComponentB і успадковуєте ComponentA, життєвий цикл OnInit від ComponentA не запускається, поки ви явно не викличете його, навіть якщо у вас є цей життєвий цикл OnInit для ComponentB.

Виклик методів супер / базових компонентів

Для того щоб мати метод ngOnInit () від Fire ComponentA, нам потрібно використовувати ключове слово super, а потім викликати потрібний нам метод, який у даному випадку - ngOnInit. Супер ключове слово стосується екземпляра компонента, який успадковується, від якого в цьому випадку буде ComponentA.


5

якщо ви читаєте через бібліотеки CDK та бібліотеки матеріалів, вони використовують успадкування, але не стільки для самих компонентів, проекція контенту є королем IMO. дивіться це посилання https://blog.angular-university.io/angular-ng-content/, де написано "ключова проблема цього дизайну"

Я знаю, що це не відповідає на ваше запитання, але я дійсно думаю, що слід уникати успадкування / розширення компонентів . Ось мої міркування:

Якщо абстрактний клас, розширений двома або більше компонентами, містить загальну логіку: скористайтеся сервісом або навіть створіть новий клас машинопису, який можна поділити між двома компонентами.

Якщо абстрактний клас ... містить загальні змінні або функції onClicketc, тоді між html двох розширень перегляду компонентів буде дублювання. Це погана практика, і те, що спільний html потрібно розбити на Компоненти. Ці компоненти (частини) можна розділити між двома компонентами.

Чи пропускаю я інших причин наявності абстрактного класу для компонентів?

Прикладом, який я нещодавно бачив, були компоненти, що розширюють функцію автоматичної підписки:

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()метод самого компонента, що так чи інакше вимагало додаткової логіки.


Гаразд, я розумію вашу розмову і погоджуюся, що агрегація майже вирішує всі проблеми, які, здається, потребують успадкування. І завжди цікаво розглядати компоненти як невеликі частини програми, які можна стикувати різними способами. Але у випадку з проблемою проблема полягає в тому, що я не маю контролю / доступу до модифікацій компонента, який я хочу успадкувати (це третій компонент), тоді агрегація стане нездійсненною, і успадкування було б ідеальним рішенням.
Фернандо Ліал

чому ви не можете просто створити новий компонент, який інкапсулює цей компонент сторонньої сторони? Який ваш третій компонент не цікавить? напр., <мій-календар [речі] = речі> <сторонній календар [речі] = речі> </ ..> </ ..>
Роберт Кінг

@robertking повторювати себе - це дуже слабкий зразок ... Ось чому ви почнете ненавидіти свою роботу .. замість того, щоб насолоджуватися нею.
Даріуш Філіпіак

Як на мене, це гарна ідея розширити компоненти, якщо ви хочете мати однакові параметри вводу / виводу для набору компонентів, щоб вони могли поводитись як один. Наприклад, у мене є кілька етапів реєстрації (облікові даніStep, addressStep, selectBenefitsStep). Усі вони повинні мати однакові параметри вводу (stepName, actionButtons ...) та Outputs (завершити, скасувати).
Sergey_T

@Sergey_T Ви могли б розглянути один компонент з ng select та проекцією контенту? Крім того, повторення декількох даних не здається, що ви дійсно економите на великій кількості функціональних можливостей TBH.
Роберт Кінг

2

Якщо хтось шукає оновлене рішення, відповідь Фернандо майже ідеальна. За винятком того ComponentMetadata, що застаріло. ВикористанняComponent замість цього працювало на мене.

Повний 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

Компоненти можна розширити так само, як і успадкування класу typecript, тільки що вам слід перекрити селектор з новим іменем. Усі властивості Input () та Output () з батьківського компонента працюють як звичайні

Оновлення

@Component - декоратор,

Декоратори застосовуються під час декларування класу не на об'єктах.

В основному, декоратори додають метадані до об’єкта класу, до яких неможливо отримати доступ через успадкування.

Якщо ви хочете домогтися спадкування декораторів, я б запропонував написати спеціальний декоратор. Щось на зразок наведеного нижче прикладу.

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);
        }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.