注意:有一个open issue related to this,但是已经开了将近5年了。
根本问题是渲染器没有将_ngcontent-app-c123 属性应用于动态组件。因此,您需要 ::ng-deep 来避免生成针对(并需要)该属性选择器的 css。
解决方案 1:在其周围放置一个包装器。
这是显而易见的解决方案,但您最终会得到一个额外的包装器。
<div id="usersettings">
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
</div>
然后,您可以使用 #usersettings 为该 div 设置样式。
解决方案 2:自定义指令以应用 _ngcontent 属性。
如果您知道门户只会显示在一个地方(而不是四处移动),那么以下将起作用。请注意,这个答案是我自己对上述问题的回答。
我发现使用ComponentPortal 是生成动态组件的最简单和最好的方法,然后您可以轻松地将它们附加到ng-template 元素。如果您还没有使用它,为了简单起见,我推荐它。
创建ComponentPortal 非常简单:
ngAfterViewInit() {
this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);
}
然后你像这样渲染它:
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
您可以通过mechanism described here 注入依赖项。
(注意 > 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 并递归调用自身以消除树。基本上它是ng-template [cdkPortalOutlet]="node.portal" 的*ngFor 循环。
我开始(天真地)急切地为树创建所有ComponentPortal。这种方式的问题是在我创建树时正确的组件实例注入器不可用。当您创建 ComponentPortal 时,您的组件实际上并未实例化。这意味着组件注入服务 - 特别是 Renderer2 并不是您真正想要的服务。事实上,当我尝试@SkipSelf() private renderer2: Renderer2 时,它会一直跳到最外层的动态组件。
所以我意识到我需要避免创建组件门户,直到实际的主机组件正在“运行”:
这是最初尝试的样子(使用急切创建的门户实例):
<ng-template [cdkPortalOutlet]="pagenode.portalInstance"></ng-template>
然后我意识到我可以制作我自己的门户指令来做我想要的以及更多!
<ng-template [dynamicComponentOutlet]="pagenode"></ng-template>
注意我是如何传入节点而不是门户实例的。
那么这个指令会做的是:
- 采用预呈现的
pagenode 表示动态组件的仅定义(及其子组件)
- 第一次尝试附加门户时,它实际上会使用正确的父 Injector 创建
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 属性的部分。
就是这样!当然,如果他们将来修复(或更改封装)这个问题,你只需要在一个地方更新它。