Динамическое добавление и удаление компонентов в Angular

В текущих официальных документах показано только, как динамически изменять компоненты внутри тега <ng-template>. https://angular.io/guide/dynamic-component-loader

То, чего я хочу достичь, скажем, у меня есть 3 компонента: header, section и footer со следующими селекторами:

<app-header>
<app-section>
<app-footer>

А затем есть 6 кнопок, которые будут добавлять или удалять каждый компонент: Add Header, Add Section и Add Footer

и когда я нажимаю Add Header, страница добавляет <app-header> к странице, которая ее отображает, поэтому страница будет содержать:

<app-header>

А потом, если я дважды нажму Add Section, страница будет содержать:

<app-header>
<app-section>
<app-section>

И если я нажму Add Footer, страница будет теперь содержать все эти компоненты:

<app-header>
<app-section>
<app-section>
<app-footer>

Можно ли добиться этого в Angular? Обратите внимание, что ngFor - это не то решение, которое я ищу, поскольку оно позволяет добавлять на страницу одни и те же компоненты, а не разные компоненты.

ОБНОВЛЕНИЕ: ngIf и ngFor это не решение, которое я ищу, так как шаблоны уже предопределены. Я ищу что-то вроде стека component или array из component, где мы можем легко добавлять, удалять и изменять любой индекс array.

ОБНОВЛЕНИЕ 2: Чтобы сделать это более ясным, давайте иметь другой пример того, почему ngFor не работает. Допустим, у нас есть следующие компоненты:

<app-header>
<app-introduction>
<app-camera>
<app-editor>
<app-footer>

Теперь появился новый компонент, <app-description>, который пользователь хочет вставить между ними и <app-editor>. ngFor работает, только если есть один и тот же компонент, который я хочу повторять снова и снова. Но для разных компонентов ngFor здесь не работает.

Ответ 1

То, чего вы пытаетесь достичь, может быть сделано путем динамического создания компонентов с помощью ComponentFactoryResolver, а затем ввода их в ViewContainerRef. Один из способов сделать это динамически - передать класс компонента в качестве аргумента вашей функции, который будет создавать и вводить компонент.

См. пример ниже:

import {
  Component,
  ComponentFactoryResolver, Type,
  ViewChild,
  ViewContainerRef
} from '@angular/core';

// Example component (can be any component e.g. app-header app-section)
import { DraggableComponent } from './components/draggable/draggable.component';

@Component({
  selector: 'app-root',
  template: `
    <!-- Pass the component class as an argument to add and remove based on the component class -->
    <button (click)="addComponent(draggableComponentClass)">Add</button>
    <button (click)="removeComponent(draggableComponentClass)">Remove</button>

    <div>
      <!-- Use ng-template to ensure that the generated components end up in the right place -->
      <ng-template #container>

      </ng-template>
    </div>

  `
})
export class AppComponent {
  @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;

  // Keep track of list of generated components for removal purposes
  components = [];

  // Expose class so that it can be used in the template
  draggableComponentClass = DraggableComponent;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) {
  }

  addComponent(componentClass: Type<any>) {
    // Create component dynamically inside the ng-template
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
    const component = this.container.createComponent(componentFactory);

    // Push the component so that we can keep track of which components are created
    this.components.push(component);
  }

  removeComponent(componentClass: Type<any>) {
    // Find the component
    const component = this.components.find((component) => component.instance instanceof componentClass);
    const componentIndex = this.components.indexOf(component);

    if (componentIndex !== -1) {
      // Remove component from both view and array
      this.container.remove(this.container.indexOf(component));
      this.components.splice(componentIndex, 1);
    }
  }
}

Примечания:

  • Если вы хотите упростить удаление компонентов позже, вы можете отслеживать их в локальной переменной, см. this.components. В качестве альтернативы вы можете перебрать все элементы внутри ViewContainerRef.

  • Вам необходимо зарегистрировать свой компонент в качестве компонента ввода. В вашем модуле определите свой компонент как entryComponent (entryComponents: [DraggableComponent]).

Пример выполнения: https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5

Для получения дополнительной информации: https://angular.io/guide/dynamic-component-loader

Ответ 2

Я создал демо, чтобы показать динамический процесс добавления и удаления. Родительский компонент динамически создает дочерние компоненты и удаляет их.

Нажмите для демонстрации

Родительский компонент

import { ComponentRef, ComponentFactoryResolver, ViewContainerRef, ViewChild, Component } from "@angular/core";

@Component({
    selector: 'parent',
    template: '
    <button type="button" (click)="createComponent()">
        Create Child
    </button>
    <div>
        <ng-template #viewContainerRef></ng-template>
    </div>
  '
})
export class ParentComponent implements myinterface {

    @ViewChild('viewContainerRef', { read: ViewContainerRef }) VCR: ViewContainerRef;

    //manually indexing the child components for better removal
    //although there is by-default indexing but it is being avoid for now
    //so index is a unique property here to identify each component individually.
    index: number = 0;

    // to store references of dynamically created components
    componentsReferences = [];

    constructor(private CFR: ComponentFactoryResolver) {
    }

    createComponent() {

        let componentFactory = this.CFR.resolveComponentFactory(ChildComponent);
        let componentRef: ComponentRef<ChildComponent> = this.VCR.createComponent(componentFactory);
        let currentComponent = componentRef.instance;

        currentComponent.selfRef = currentComponent;
        currentComponent.index = ++this.index;

        // prividing parent Component reference to get access to parent class methods
        currentComponent.compInteraction = this;

        // add reference for newly created component
        this.componentsReferences.push(componentRef);
    }

    remove(index: number) {

        if (this.VCR.length < 1)
            return;

        let componentRef = this.componentsReferences.filter(x => x.instance.index == index)[0];
        let component: ChildComponent = <ChildComponent>componentRef.instance;

        let vcrIndex: number = this.VCR.indexOf(componentRef)

        // removing component from container
        this.VCR.remove(vcrIndex);

        this.componentsReferences = this.componentsReferences.filter(x => x.instance.index !== index);
    }
}

Дочерний компонент

@Component({
    selector: 'child',
    template: '
    <div>
    <h1 (click)="removeMe(index)">I am a Child, click to Remove</h1>
    </div>
    '
})
export class ChildComponent {

    public index: number;
    public selfRef: ChildComponent;

    //interface for Parent-Child interaction
    public compInteraction: myinterface;

    constructor() {
    }

    removeMe(index) {
        this.compInteraction.remove(index)
    }
}

// Interface
export interface myinterface {
    remove(index: number);
}

добавить ссылки на app.module.ts

@NgModule({
  declarations: [

    ParentComponent,
    ChildComponent

  ],
  imports: [

    //if using routing then add like so
    RouterModule.forRoot([
      { path: '', component: ParentComponent }
    ]),

  ],
  entryComponents: [

    ChildComponent,  

  ],