Примечание. Существует открытая проблема, связанная с этим, но он был открыт в течение почти 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
, он переходил к самому внешнему динамическому компоненту.
Итак, я понял, что мне нужно избегать создания портала компонентов до тех пор, пока фактический хост-компонент не будет «запущен»:
Вот как выглядела первоначальная попытка (с жадно созданными portInstances):
<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
, если вы не можете встроить это в существующий проект.
И это все! Конечно, если они исправят (или изменят инкапсуляцию) это в будущем, вам нужно будет обновить это только в одном месте.
person
Simon_Weaver
schedule
01.07.2021