Как расширить / наследовать компоненты?

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: аннотации компонентов 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

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

Этот ответ Тьерри Темплиера - альтернативный способ обойти проблему.

После некоторых вопросов с Тьерри Темплиером я пришел к следующему рабочему примеру, который соответствует моим ожиданиям в качестве альтернативы ограничения наследования, упомянутого в этом вопросе:

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() {
  }
}

Плункр с полным примером.

Фернандо Лил
источник
3
Я предполагаю, что это не будет совместимо с автономным компилятором шаблонов.
Гюнтер Цохбауэр
@ GünterZöchbauer, у меня нет знаний об «автономном шаблоне компилятора» Angular2. Но я думаю, что это может быть несовместимо, и это будет альтернативным вариантом. Где был бы полезен режим «автономный компилятор шаблонов» Angular2? Вы можете показать мне что-нибудь, чтобы лучше понять это? Таким образом, я могу понять важность этой совместимости для моего проекта.
Фернандо Лил
Автономный компилятор шаблонов (OTC) еще не функционирует, даже если он уже включен в RC.3. OTC будет анализировать декораторы и генерировать код на этапе сборки, когда генерируется развертываемое. OTC позволяет удалить анализатор и компилятор Angular2, которые обрабатывают декораторы и привязки во время выполнения, что приводит к заметному меньшему размеру кода и более быстрой инициализации приложений и компонентов. OTC, вероятно, станет пригодным для использования с одним из следующих обновлений.
Гюнтер Цохбауэр
1
@ GünterZöchbauer, теперь я понимаю важность поддержания функциональности, совместимой с OTC. Это будет предварительная компиляция угловых декораторов, уменьшающих накладные расходы на инициализацию компонентов. Я хотел бы знать о функционировании этого процесса и потому что решение этого ответа не будет совместимо с OTC? Как проходит предварительная компиляция декораторов? Обладая этими знаниями, мы могли бы придумать что-нибудь, чтобы сохранить эту функциональную альтернативу OTC. Спасибо за пояснение!
Фернандо Лил
24

Angular 2 версия 2.3 была только что выпущена, и она включает в себя нативное наследование компонентов. Похоже, вы можете наследовать и переопределять все, что вы хотите, за исключением шаблонов и стилей. Некоторые ссылки:

Даниэль Гриском
источник
Одна «ошибка» здесь возникает, когда вы забыли указать новый «селектор» в дочернем компоненте. Вы получите ошибку во время выполнения, More than one component matched on this elementесли вы этого не сделаете.
Аэльфинн
@TheAelfinn Да: у каждого компонента должна быть полная спецификация в @Component()теге. Но вы можете поделиться .html или .css, ссылаясь на тот же файл, если хотите. В общем, это большой плюс.
Даниэль Гриском
Во второй ссылке scotch.io/tutorials/component-inheritance-in-angular-2 автор утверждает, что компоненты наследуют сервисы , внедренные зависимостями своих родителей, а мой код подсказывает обратное. Можете ли вы подтвердить, что это поддерживается?
Аэльфинн
18

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

  • Миксины сочетаются более гибко, т.е. вы можете смешивать и сочетать миксины на существующих компонентах или комбинировать миксины для формирования новых компонентов
  • Состав Mixin остается простым для понимания благодаря его очевидной линеаризации к иерархии наследования классов
  • Вы можете легче избежать проблем с декораторами и аннотациями, которые мешают наследованию компонентов ( обсуждение )

Я настоятельно рекомендую вам прочитать объявление TypeScript 2.2 выше, чтобы понять, как работают Mixins. Связанные обсуждения в угловых проблемах GitHub предоставляют дополнительные детали.

Вам понадобятся эти типы:

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

export class MixinRoot {
}

И затем вы можете объявить Mixin как этот Destroyablemixin, который помогает компонентам отслеживать подписки, которые должны быть расположены в 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это необходимо только тогда, когда вы хотите, чтобы extendMixin композиции. Вы можете легко расширить несколько миксинов, например A extends B(C(D)). Это очевидная линеаризация миксинов, о которой я говорил выше, например, вы эффективно составляете иерархию наследования A -> B -> C -> D.

В других случаях, например, когда вы хотите создать Mixins для существующего класса, вы можете применить Mixin следующим образом:

const MyClassWithMixin = MyMixin(MyClass);

Тем не менее, я обнаружил, что первый способ работает лучше всего, Componentsи Directives, так как они также должны быть украшены @Componentили в @Directiveлюбом случае.

Йоханнес Рудольф
источник
это круто! спасибо за предложение. здесь MixinRoot просто используется как заполнитель пустого класса? просто хочу убедиться, что мое понимание верно.
Алекс Локвуд
@AlexLockwood Да, пустой заполнитель класса - это именно то, для чего я его использую. Я бы с радостью избежал его использования, но пока я не нашел лучшего способа сделать это.
Йоханнес Рудольф
2
Я закончил тем, что использовал function Destroyable<T extends Constructor<{}>>(Base = class { } as T). Таким образом, я могу создавать миксины, используя extends Destroyable().
Алекс Локвуд
1
Это выглядит очень хорошо, однако, похоже, что сборка AoT (Cli1.3) удаляет ngOnDestroy из DashBoardComponent, поскольку он никогда не вызывается. (то же самое относится и к ngOnInit)
dzolnjan
спасибо за это решение. Однако после сборки prod с ionic или angular-cli миксин как-то не работает, как будто он не был расширен.
Хан Че,
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
am0wa
источник
2
Это именно то, что мне нужно. Как будет выглядеть декоратор @Component для BasePanelComponent? Может ли он ссылаться на другие файлы HTML / CSS? Может ли он ссылаться на те же файлы html / css, на которые ссылается MyPanelComponent?
ebhh2001
1
Это не наследует @Input()и @Output()декораторы, не так ли?
Леон Адлер
10

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

watzon
источник
Понял, я постараюсь в ближайшие дни повторить мой проект angular2 и убедиться, что функция запросов больше не связана с проблемами проекта в Git, и если нет, то я составлю запрос на ресурс, так как он кажется мне очень интересным характерная черта. Есть ли дополнительные аргументы, чтобы сделать самый интересный запрос?
Фернандо Лил
1
Что касается машинописного текста ресурса наследования, который я уже использую в своем первоначальном решении ( export class MyPanelComponent extends BasePanelComponent), проблема заключается только в том, что аннотации / декораторы не наследуются.
Фернандо Лил
1
Да, я действительно не знаю, что еще ты мог бы добавить. Мне нравится идея иметь новый декоратор (что-то вроде @SubComponent()), который помечает класс как подкомпонент, или иметь дополнительное поле в @Componentдекораторе, которое позволяет вам ссылаться на родительский компонент для наследования.
Watzon
1
В проект на GitHub добавлен запрос функции angular2: аннотации компонентов Extend / Inherit angular2 # 7968
Фернандо Лил,
9

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

Компонент наследует только логику класса:

  • Все метаданные в декораторе @Component не наследуются.
  • Свойства компонента @Input и свойства @Output наследуются.
  • Жизненный цикл компонента не наследуется.

Эти функции очень важно иметь в виду, поэтому давайте рассмотрим каждую из них независимо.

Компонент наследует только логику класса

Когда вы наследуете Компонент, вся логика внутри одинаково наследуется. Стоит отметить, что только открытые члены наследуются, поскольку частные члены доступны только в классе, который их реализует.

Все метаданные в декораторе @Component не наследуются

Тот факт, что метаданные не наследуются, на первый взгляд может показаться нелогичным, но, если подумать, это действительно имеет смысл. Если вы наследуете от компонента Component (componentA), вы бы не хотели, чтобы селектор ComponentA, от которого вы наследуете, заменил селектор ComponentB, который является классом, который наследует. То же самое можно сказать и о template / templateUrl, а также о style / styleUrls.

Свойства компонента @Input и @Output наследуются

Это еще одна особенность, которая мне очень нравится в компоненте Inheritance в Angular. В простом предложении, когда у вас есть пользовательские свойства @Input и @Output, эти свойства наследуются.

Жизненный цикл компонента не наследуется

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

Вызов методов Super / Base Component

Чтобы получить метод ngOnInit () из огня ComponentA, нам нужно использовать ключевое слово super, а затем вызвать нужный нам метод, в данном случае ngOnInit. Ключевое слово super ссылается на экземпляр компонента, который наследуется, от которого в этом случае будет ComponentA.

Масуд Бимар
источник
5

Если вы читаете библиотеки CDK и библиотеки материалов, они используют наследование, но не столько для самих компонентов, но и для контент-проекции - главное ИМО. см. эту ссылку 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> <сторонний календарь [stuff] = stuff> </ ..> </ ..>
Роберт
@ robertking повторять себя - это очень слабый шаблон ... Вот почему вы начинаете ненавидеть свою работу ... вместо того, чтобы наслаждаться ею.
Дариуш Филипяк
Что касается меня, это хорошая идея, чтобы расширить компоненты в случае, если вы хотите иметь одинаковые параметры ввода / вывода для набора компонентов, чтобы они могли вести себя как один. Например, у меня есть несколько шагов регистрации (credentialsStep, addressStep, selectBenefitsStep). Все они должны иметь одинаковые параметры ввода (stepName, actionButtons ...) и выходы (завершить, отменить).
Sergey_T
@Sergey_T не могли бы вы рассмотреть один компонент с ng select и проекцией контента? Кроме того, повторение нескольких входов не похоже на то, что вы действительно экономите на функциональности TBH.
Роберт
2

Если кто-то ищет обновленное решение, ответ Фернандо в значительной степени идеален. За исключением того, что ComponentMetadataбыло объявлено устаревшим. Использование Componentвместо этого работало для меня.

Полный CustomDecorator.tsфайл Custom Decorator выглядит следующим образом:

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!
}
rhyneav
источник
Разве нестандартные декораторы не очень обескуражены? Из многих других сообщений / тем это решение было помечено как полностью неправильное, потому что AOT не будет их поддерживать?
Тернови
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 {

}
Mizuwokiru
источник
1

Компоненты могут быть расширены так же, как наследование классов машинописного текста, просто вы должны переопределить селектор новым именем. Все свойства 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);
        }
}
митталь бхатт
источник