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 и другие вещи.

Перед новым угловым выпуском я решил эту проблему, используя решение из 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: Как представляется в недавней угловой версии, все компоненты, которые должны быть созданы / внедрены динамически, должны быть предварительно определены в 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();
    }    
  }
}

Смотрите также Пример Plunker

Пример с пользовательским компонентом

Для компиляции AOT смотрите эти темы

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

Другие вопросы по тегам