Динамические компоненты с ViewEncapsulation.Emulated + CDKPortal нельзя стилизовать (вообще) без использования ::ng-deep

avatar
Simon_Weaver
1 июля 2021 в 21:06
243
1
0

Я создаю динамические компоненты, используя Angular Material Portal ComponentPortal.

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) позиционировал его, но он не может.

Примечание. Это относится не только к порталам — если я создам компонент вручную, проблема останется той же.

Источник

Ответы (1)

avatar
Simon_Weaver
1 июля 2021 в 21:06
0

Примечание: существует открытая проблема, связанная с этим, но она существует уже почти 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 (о котором и идет речь в этом выпуске!).

Вот мое решение:

  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, если вы не можете встроить его в существующий проект.

И все! Конечно, если они исправят (или изменят инкапсуляцию) это в будущем, вам нужно будет обновить это только в одном месте.