【问题标题】:Dynamic components with ViewEncapsulation.Emulated + CDKPortal cannot be styled (at all) without using ::ng-deepViewEncapsulation.Emulated + CDKPortal 的动态组件不能在不使用 ::ng-deep 的情况下(根本)设置样式
【发布时间】:2021-07-01 21:06:00
【问题描述】:

我正在使用Angular Material PortalComponentPortal 创建动态组件。

ngAfterViewInit() {
     this.userSettingsPortal = new ComponentPortal(UserSettingsComponent, null, this.hostInjector);
}

然后我这样显示:

<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>

假设这为我生成了一个UserSettingsComponent 组件,我想应用一个边距。

app-user-settings 
{
   margin: 20px;
   outline: 2px solid red;
}

除非我使用非常笨拙的::ng-deep,否则这不起作用。通常::ng-deep 用于“刺穿”元素的样式黑盒,但在这种情况下我没有这样做。我只是想让(UserSettings 的)主机组件定位它,但它不能。

注意:这不是特定于门户网站的 - 如果我手动创建组件,它仍然是同样的问题。

【问题讨论】:

    标签: angular angular-material


    【解决方案1】:

    注意:有一个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 属性(这是本期的全部问题!)。

    这是我的解决方案:

    1. 首先我需要创建一个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 属性的部分。

    就是这样!当然,如果他们将来修复(或更改封装)这个问题,你只需要在一个地方更新它。

    【讨论】:

      猜你喜欢
      • 2021-08-10
      • 2020-09-07
      • 2020-11-08
      • 2022-12-17
      • 1970-01-01
      • 2022-01-27
      • 2023-01-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多