Примечание: существует открытая проблема, связанная с этим, но она существует уже почти 5 лет.
Основная проблема заключается в том, что средство визуализации не применяет атрибут _ngcontent-app-c123
к динамическому компоненту. Поэтому вам нужно ::ng-deep
, чтобы избежать генерации css, который нацелен (и требует) на этот селектор атрибутов.
Решение 1. Накройте его оберткой.
Это очевидное решение, но вы получите дополнительную обертку.
<div id="usersettings">
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
</div>
Затем вы можете стилизовать этот div так, как вы ожидаете, с помощью #usersettings
.
Решение 2. Пользовательская директива для применения атрибута _ngcontent
.
Если вы знаете, что портал будет отображаться только в одном месте (а не перемещаться), тогда будет работать следующее. Обратите внимание, что этот ответ принадлежит мне по вышеупомянутой проблеме.
Я считаю использование ComponentPortal
самым простым и лучшим способом создания динамических компонентов, и затем вы можете легко прикрепить их к элементу ng-template
. Если вы еще не используете его, я рекомендую его для простоты.
Создать ComponentPortal довольно просто:
ngAfterViewInit() {
this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);
}
Затем вы визуализируете это следующим образом:
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
Вы можете внедрить зависимости с помощью механизма, описанного здесь.
(Обратите внимание, что для> Angular 10 вы не должны использовать устаревший ответ с WeakMap).
Важные соображения по конструкции/дереву инжектора
Вы можете создавать только один динамический компонент или целое дерево. В любом случае вам действительно нужно передать инжектор вашего хост-компонента в конструктор ComponentPortal
, чтобы получить «нормальное» поведение, которое вы ожидаете в дереве инжектора компонентов.
Как ни странно, приведенный выше пример (из документации CDK) этого не делает. Я думаю, что причина в том, что одно из основных применений порталов заключается в том, чтобы взять компонент, определенный в одном месте, и поместить его на свою страницу, где вы хотите. Так что в этом случае родительский инжектор не имеет смысла.
Если вы создаете компонент динамически и размещаете его в том же компоненте, вам действительно следует использовать следующий конструктор:
const componentPortal = new ComponentPortal(component, null, parentInjector);
Однако, если вы создаете дерево динамических компонентов, это становится логистической проблемой! Вы должны загромождать свои хост-компоненты всем этим кодом parentInjector.
Мое решение проблемы ViewEncapsulation.Emulated
Мое приложение представляет собой графический пользовательский интерфейс для разработки страницы из таких компонентов, как сетки, таблицы, изображения, видео и т. д.
Модель определяется как дерево «визуализированных узлов», что-то вроде следующего.
Как видите, у меня есть ComponentPortal
в каждом узле:
export type RenderedPage =
{
children: (RenderedPageNode | undefined)[];
}
// this corresponds to a node in the tree
export type RenderedPageNode =
{
portal: ComponentPortal;
children: RenderedPageNode[] | undefined;
}
КСТАТИ. Эта модель отображается компонентом, который перебирает children
и рекурсивно вызывает сам себя, чтобы удалить дерево. По сути, это цикл *ngFor
из ng-template [cdkPortalOutlet]="node.portal"
.
.
Я начал (наивно) создавать все ComponentPortal
для дерева. Проблема с этим способом заключается в том, что правильный инжектор экземпляра компонента недоступен во время создания дерева. Когда вы создаете ComponentPortal
, ваш компонент фактически не создается. Это означает, что службы, внедряемые компонентами, особенно Renderer2
, не являются теми, которые вам действительно нужны. На самом деле, когда я пробовал @SkipSelf() private renderer2: Renderer2
, он переходил к самому внешнему динамическому компоненту.
Итак, я понял, что мне нужно избегать создания портала компонентов до тех пор, пока фактический хост-компонент не будет "запущен":
Вот как выглядела первоначальная попытка (с созданными экземплярами портала):
<ng-template [cdkPortalOutlet]="pagenode.portalInstance"></ng-template>
Затем я понял, что могу просто создать собственную директиву портала, чтобы делать именно то, что мне нужно и многое другое!
<ng-template [dynamicComponentOutlet]="pagenode"></ng-template>
Обратите внимание, что я передаю узел, а не экземпляр портала.
Итак, что будет делать эта директива:
- Возьмите предварительно визуализированный
pagenode
, представляющий динамическое определение только (и его дочерние элементы)
- При первой попытке присоединения портала фактически создается экземпляр
ComponentPortal
с правильным родительским инжектором
- Поскольку контекст инжектора выхода
dynamicComponentOutlet
является хост-компонентом, он также может генерировать и применять атрибут _ngcontent-app-c338
(о котором и идет речь в этом выпуске!).
Вот мое решение:
- Сначала мне нужно было создать
LazyComponentOutlet
, который содержит заполнитель для ComponentPortal
, а также любые данные, необходимые для его создания. Я только что позвонил по этому адресу params
, потому что решать вам. Я также не включаю определение ComponentPortalParams
по той же причине. Как минимум, он должен включать тип компонента.
// this corresponds to a node in the tree
export type RenderedPageNode =
{
// lazily instantiated portal
lazyPortal: LazyComponentPortal;
children: RenderedPageNode[] | undefined;
}
export type LazyComponentPortal =
{
// the actual ComponentPortal which initially is undefined until the directive initializes it
componentPortal: ComponentPortal<any> | undefined;
// whatever we need to create a component
params: ComponentPortalParams // this is application specific to whatever you need
}
Затем атрибут DynamicComponentPortalHost
(переименуйте его, как вам угодно):
Обратите внимание, что это навеяно способом наследования портала в portal-directives.ts
@Directive({
selector: '[dynamicComponentOutlet]',
exportAs: 'rrDynamicComponentHost',
inputs: ['dynamicComponentOutlet: rrDynamicComponentHost'],
providers: [{
provide: CdkPortalOutlet,
useExisting: DynamicComponentPortalHostDirective
}]
})
export class DynamicComponentPortalHostDirective extends CdkPortalOutlet {
constructor(
// parameters required by CdkPortalOutlet constructor (passed via super)
_componentFactoryResolver: ComponentFactoryResolver,
_viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) _document: any,
// renderer inherited from host component (where the ng-template is defined)
private renderer2: Renderer2,
// injector (from parent) to use as a parent injector for our ComponentPortal
private injector: Injector,
// my own service to create a ComponentPortal
// it's up to you how you create a ComponentPortal inside this
private componentPortalFactory: ComponentPortalFactoryService)
{
super(_componentFactoryResolver, _viewContainerRef, _document);
// need to subscribe immediately because ngOnInit is too late
// when the component is attached we can immediately grab its element
this._subscription.add(this.attached.subscribe((component: ComponentRef<any> | null) => {
if (component)
{
// use parent renderer to determine the correct content attribute for us
// to do this we just render a fake element and 'borrow' it's first (and only) attribute
// _ngcontent-app-c338
const contentAttr = this.renderer2.createElement('div').attributes[0].name;
renderer2.setAttribute(component.location.nativeElement, contentAttr, '');
}
}));
}
_subscription = new Subscription()
ngOnDestroy()
{
this._subscription.unsubscribe();
}
@Input('dynamicComponentOutlet')
set dynamicComponentOutlet(pageNode: RenderedPageNode)
{
// if we haven't yet instantiated a ComponentPortal instance create one
if (!pageNode.lazyPortal.componentPortal)
{
// create component portal
// how you do this is up to you, just be sure to use the constructor that includes injector
const componentPortal = this.componentPortalFactory.createComponentPortal(pageNode.lazyPortal.params, this.injector);
// we now have an actual instance of ComponentPortal, so save a reference
value.portal.componentPortal = componentPortal;
}
// set the ComponentPortal on the actual 'inherited cdkPortal'
this.portal = value.portal.componentPortal!;
}
}
Наконец, этот метод работает так же хорошо для одного элемента, а не для дерева. Или вы можете извлечь только ту часть, которая отображает атрибут ngContent
, если вы не можете встроить его в существующий проект.
И все! Конечно, если они исправят (или изменят инкапсуляцию) это в будущем, вам нужно будет обновить это только в одном месте.