Подтвердить что ты не робот

Angular2: вставьте динамический компонент в качестве дочернего элемента контейнера в DOM

Есть ли способ динамически вставлять компонент в качестве дочернего (не брата) тега DOM в Angular 2?

Здесь есть много примеров, чтобы вставить динамический компонент в качестве родного брата данного тега ViewContainerRef, например (с RC3):

@Component({
  selector: '...',
  template: '<div #placeholder></div>'
})
export class SomeComponent {
  @ViewChild('placeholder', {read: ViewContainerRef}) placeholder;

  constructor(private componentResolver: ComponentResolver) {}

  ngAfterViewInit() {
    this.componentResolver.resolveComponent(MyDynamicComponent).then((factory) => {
        this.componentRef = this.placeholder.createComponent(factory);
    });
  }
}

Но это порождает DOM, подобный:

<div></div>
<my-dynamic-component></my-dynamic-component>

Ожидаемый результат:

<div>
    <my-dynamic-component></my-dynamic-component>
</div>

Использование SomeComponent ViewContainerRef имеет тот же результат, он по-прежнему вставляет сгенерированный компонент как родной, а не дочерний. Я был бы в порядке с решением, где шаблон пуст, а динамические компоненты вставлены в шаблон (в теге селектора компонентов).

Структура DOM очень важна при использовании библиотек, таких как ng2-dragula, перетащить из списка динамических компонентов и извлечь выгоду из обновлений модели. Дополнительный div находится в списке перетаскиваемых элементов, но вне модели, нарушая логику перетаскивания.

Некоторые говорят, что это невозможно (c.f. этот комментарий), но это звучит как очень удивительное ограничение.

4b9b3361

Ответ 1

TL; DR: замените <div #placeholder></div> на <div><ng-template #placeholder></ng-template></div> чтобы вставить его в div.

Вот рабочий пример стекаблица (Angular 6) и соответствующий код:

@Component({
  selector: 'my-app',
  template: '<div><ng-template #container></ng-template></div>'
})
export class AppComponent implements OnInit {

    @ViewChild('container', {read: ViewContainerRef}) viewContainer: ViewContainerRef;

    constructor(private compiler: Compiler) {}

    ngOnInit() {
      this.createComponentFactory(MyDynamicComponent).then(
        (factory: ComponentFactory<MyDynamicComponent>) => this.viewContainer.createComponent(factory),
        (err: any) => console.error(err));
    }

    private createComponentFactory(/*...*/) {/*...*/}

}

Кажется, что <ng-container #placeholder></ng-container> также работает (замените ng-template на ng-container). Мне нравится такой подход, потому что <ng-container> явно адресован этому сценарию использования (контейнеру, в котором нет тега) и может использоваться в других ситуациях, таких как NgIf без переноса в реальный тег.


PS: @GünterZöchbauer направил меня к правильному обсуждению в комментарии, и я наконец-то ответил на свой вопрос.


Редактировать [2018-05-30]: обновлена ссылка на stackblitz, чтобы иметь работающий, обновленный пример.

Ответ 2

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

Идея состоит в том, чтобы использовать ссылку на элемент вновь созданного компонента и добавить его в текущий элемент ref с помощью службы рендеринга. Мы можем получить объект компонента через его свойство инжектора.

Вот код:

@Directive({
    selector: '[mydirective]'
})
export class MyDirectiveDirective {
    constructor(
        private cfResolver: ComponentFactoryResolver,
        public vcRef: ViewContainerRef,
        private renderer: Renderer2
    ) {}

    public appendComponent() {
        const factory = 
        this.cfResolver.resolveComponentFactory(MyDynamicComponent);
        const componentRef = this.vcRef.createComponent(factory);
        this.renderer.appendChild(
           this.vcRef.element.nativeElement,
           componentRef.injector.get(MyDynamicComponent).elRef.nativeElement
        );
    }
}

Ответ 3

Мой грязный способ, просто сохраните ссылку на динамически созданный компонент (sibling) и переместите его с помощью vanilla JS:

  constructor(
    private el: ElementRef,
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver,
  ) {}

  ngOnInit() {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
      MyDynamicComponent,
    );
    const componentRef = this.viewContainerRef.createComponent(componentFactory);
    this.el.nativeElement.appendChild(componentRef.location.nativeElement);
  }

Ответ 4

Мое решение было бы очень похоже на @Bohdan Khodakivskyi. Но я попробовал Renderer2.

  constructor(
    private el: ElementRef,
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver,
    private render: Renderer2
  ) {}

  ngOnInit() {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
      MyDynamicComponent,
    );
    const componentRef = this.viewContainerRef.createComponent(componentFactory);
    this.render.appendChild(this.el.nativeElement, componentRef.location.nativeElement)
  }

Ответ 5

Более простой подход теперь доступен с Portal, доступным в @angular/cdk.

Npm я @Angular/CDK

app.module.ts:

import { PortalModule } from '@angular/cdk/portal';

@NgModule({
  imports: [PortalModule],
  entryComponents: [MyDynamicComponent]
})

SomeComponent:

@Component({
  selector: 'some-component',
  template: '<ng-template [cdkPortalOutlet]="myPortal"></ng-template>'
})
export class SomeComponent {
  ...
  this.myPortal = new ComponentPortal(MyDynamicComponent);
}