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. этот комментарий), но это звучит как очень удивительное ограничение.
Ответы
Ответ 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);
}