Angular 2.1.0 создает дочерний компонент "на лету", динамически

То, что я пытаюсь сделать в angular 2.1.0, - это создание дочерних компонентов "на лету", которые следует вводить в родительский компонент. Например, родительский компонент lessonDetails, который содержит общие сведения для всех уроков, таких как кнопки Go to previous lesson, Go to next lesson и другие. На основе параметров маршрута содержание урока, которое должно быть дочерним компонентом, должно динамически вставляться в родительский компонент . HTML для дочерних компонентов (содержание урока) определяется как простая строка где-то снаружи, он может быть как:

export const LESSONS = {
  "lesson-1": `<p> lesson 1 </p>`,
  "lesson-2": `<p> lesson 2 </p>`
}

Проблема может быть легко решена с помощью innerHtml, имеющего что-то вроде следующего в шаблоне родительского компонента.

<div [innerHTML]="lessonContent"></div>

Где при каждом изменении параметров маршрута свойство lessonContent родительского компонента изменилось бы (содержимое (новый шаблон) будет взято из объекта LESSON), в результате чего шаблон родительского компонента будет обновлен. Это работает, но angular не будет обрабатывать содержимое, введенное через innerHtml, поэтому невозможно использовать routerLink и другие материалы.

Перед выпуском новой версии angular я решил эту проблему, используя решение от http://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/, где я использовал ComponentMetadata вместе с ComponentResolver для создания дочерних компонентов на лету, например:

const metadata = new ComponentMetadata({
  template: this.templateString,
});

Где templateString передано дочернему компоненту как свойство Input дочернему компоненту. Оба MetaData и ComponentResolver устарели/удалены в angular 2.1.0.

Таким образом, проблема заключается не только в создании динамических компонентов, как описано в нескольких связанных вопросах SO, проблема будет легче решить, если бы я определил компонент для каждого урока. Это означало бы, что мне нужно предусмотреть 100 различных компонентов для 100 различных уроков. Устаревшие метаданные обеспечивали поведение, подобное обновлению шаблона во время выполнения одного компонента (создание и уничтожение одного компонента при изменении параметров маршрута).

Обновление 1: Как показано в недавнем выпуске angular, все компоненты, которые необходимо создать/вставить динамически, должны быть предварительно определены в entryComponents в пределах @NgModule. Так, как мне кажется, связанный с вопросом выше, если мне нужно иметь 100 уроков (компоненты, которые необходимо создать динамически "на лету" ), это означает, что мне нужно предопределить 100 компонентов

Обновление 2: На основе обновления 1 это можно сделать с помощью ViewContainerRef.createComponent() следующим образом:

// lessons.ts
@Component({ template: html string loaded from somewhere })
class LESSON_1 {}

@Component({ template: html string loaded from somewhere })
class LESSON_2 {}

// exported value to be used in entryComponents in @NgModule
export const LESSON_CONTENT_COMPONENTS = [ LESSON_1, LESSON_2 ]

Теперь в родительском компоненте изменения параметров маршрута

const key = // determine lesson name from route params

/**
 * class is just buzzword for function
 * find Component by name (LESSON_1 for example)
 * here name is property of function (class)
 */

const dynamicComponent = _.find(LESSON_CONTENT_COMPONENTS, { name: key });
const lessonContentFactory = this.resolver.resolveComponentFactory(dynamicComponent);
this.componentRef = this.lessonContent.createComponent(lessonContentFactory);

Родительский шаблон выглядит так:

<div *ngIf="something" #lessonContentContainer></div>

Где lessonContentContainer декорировано свойство @ViewChildren и lessonContent оформляется как @ViewChild и инициализируется в ngAfterViewInit () как:

ngAfterViewInit () {
  this.lessonContentContainer.changes.subscribe((items) => {
    this.lessonContent = items.first;
    this.subscription = this.activatedRoute.params.subscribe((params) => {
      // logic that needs to show lessons
    })
  })
}

Решение имеет один недостаток, и все компоненты (LESSON_CONTENT_COMPONENTS) должны быть предварительно определены.
Существует ли способ использования одного компонента и изменения шаблона этого компонента во время выполнения ( при изменении параметров маршрута)?

Ответ 1

Вы можете использовать следующую директиву HtmlOutlet:

import {
  Component,
  Directive,
  NgModule,
  Input,
  ViewContainerRef,
  Compiler,
  ComponentFactory,
  ModuleWithComponentFactories,
  ComponentRef,
  ReflectiveInjector
} from '@angular/core';

import { RouterModule }  from '@angular/router';
import { CommonModule } from '@angular/common';

export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> {
    const cmpClass = class DynamicComponent {};
    const decoratedCmp = Component(metadata)(cmpClass);

    @NgModule({ imports: [CommonModule, RouterModule], declarations: [decoratedCmp] })
    class DynamicHtmlModule { }

    return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
       .then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => {
        return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
      });
}

@Directive({ selector: 'html-outlet' })
export class HtmlOutlet {
  @Input() html: string;
  cmpRef: ComponentRef<any>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }

  ngOnChanges() {
    const html = this.html;
    if (!html) return;

    if(this.cmpRef) {
      this.cmpRef.destroy();
    }

    const compMetadata = new Component({
        selector: 'dynamic-html',
        template: this.html,
    });

    createComponentFactory(this.compiler, compMetadata)
      .then(factory => {
        const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);   
        this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
      });
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

См. также Пример плунжера

Пример с настраиваемым компонентом

Для компиляции AOT см. эти потоки

См. также пример gitub Webpack AOT https://github.com/alexzuza/angular2-build-examples/tree/master/ngc-webpack