Komponen dinamis dengan ViewEncapsulation.Emulated + CDKPortal tidak dapat ditata (sama sekali) tanpa menggunakan ::ng-deep

Saya membuat komponen dinamis menggunakan ComponentPortal Angular Material Portal.

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

Lalu saya menampilkannya seperti ini:

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

Katakanlah ini menghasilkan komponen UserSettingsComponent untuk saya dan saya ingin menerapkan margin.

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

Ini tidak akan berhasil kecuali saya menggunakan ::ng-deep yang sangat kikuk. Biasanya ::ng-deep digunakan untuk 'menembus' kotak hitam gaya elemen tetapi dalam kasus ini saya tidak melakukan itu. Saya hanya ingin komponen Host (dari UserSettings) memposisikannya tetapi tidak bisa.

Catatan: Ini tidak khusus untuk portal - jika saya membuat komponen secara manual, masalahnya masih sama.


person Simon_Weaver    schedule 01.07.2021    source sumber


Jawaban (1)


Catatan: Ada masalah terbuka terkait dengan ini tetapi sudah terbuka selama hampir 5 tahun.

Masalah mendasarnya adalah penyaji tidak menerapkan atribut _ngcontent-app-c123 ke komponen dinamis. Oleh karena itu, Anda memerlukan ::ng-deep untuk menghindari pembuatan css yang menargetkan (dan memerlukan) pemilih atribut tersebut.

Solusi 1: Letakkan pembungkus di sekelilingnya.

Ini adalah solusi yang jelas, tetapi Anda akan mendapatkan pembungkus tambahan.

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

Anda kemudian dapat menata div ini seperti yang Anda harapkan dengan #usersettings.

Solusi 2: Perintah khusus untuk menerapkan atribut _ngcontent.

Jika Anda tahu portal hanya akan ditampilkan di satu tempat (tidak berpindah-pindah) maka berikut ini akan berfungsi. Harap dicatat bahwa jawaban ini adalah jawaban saya sendiri dari masalah yang disebutkan di atas.


Saya menemukan menggunakan ComponentPortal cara termudah dan terbaik untuk menghasilkan komponen dinamis, dan Anda kemudian dapat melampirkannya dengan mudah ke elemen ng-template. Jika Anda belum menggunakannya, saya merekomendasikannya untuk kesederhanaan.

Membuat ComponentPortal cukup mudah:

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

Kemudian Anda merendernya seperti ini:

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

Anda dapat memasukkan dependensi melalui mekanisme yang dijelaskan di sini. (Catatan untuk ›Angular 10 Anda tidak boleh menggunakan jawaban yang tidak digunakan lagi dengan WeakMap).

Pertimbangan Penting Desain / Pohon Injektor

Anda mungkin hanya membuat satu komponen dinamis, atau Anda mungkin membuat keseluruhan pohon. Dalam kedua kasus tersebut, Anda benar-benar harus meneruskan injektor komponen host Anda ke konstruktor ComponentPortal untuk mendapatkan perilaku 'normal' yang Anda harapkan di pohon injektor komponen.

Anehnya contoh yang ditunjukkan di atas (dari dokumen CDK) tidak melakukan hal ini. Saya pikir alasan salah satu kegunaan utama portal adalah untuk mengambil komponen yang ditentukan di satu tempat dan meletakkannya di halaman Anda di mana pun Anda inginkan. Jadi dalam hal ini injektor induk kurang masuk akal.

Jika Anda membuat komponen secara dinamis dan menempatkannya di komponen yang sama, Anda harus menggunakan konstruktor berikut:

     const componentPortal = new ComponentPortal(component, null, parentInjector);

Namun jika Anda membuat pohon komponen dinamis, hal ini akan merepotkan secara logistik! Anda harus mengacaukan komponen host Anda dengan semua kode parentInjector ini.

Solusi saya untuk masalah ViewEncapsulation.Emulated

Aplikasi saya adalah UI grafis untuk mendesain halaman dari komponen seperti kisi, tabel, gambar, video, dll.

Model didefinisikan sebagai pohon 'node yang dirender' seperti berikut. Seperti yang Anda lihat, saya memiliki ComponentPortal di setiap node:

export type RenderedPage =
{
    children: (RenderedPageNode | undefined)[];
}

// this corresponds to a node in the tree
export type RenderedPageNode =
{
    portal: ComponentPortal;
    children: RenderedPageNode[] | undefined;
}

OMONG-OMONG. Model ini ditampilkan oleh komponen yang melakukan iterasi melalui children dan secara rekursif memanggil dirinya sendiri untuk membasmi pohon. Pada dasarnya ini adalah putaran *ngFor dari ng-template [cdkPortalOutlet]="node.portal".

Saya memulai dengan (secara naif) membuat semua ComponentPortal untuk pohon itu dengan penuh semangat. Masalahnya dengan cara ini adalah injektor instance komponen yang benar tidak tersedia pada saat saya membuat pohon. Saat Anda membuat ComponentPortal, komponen Anda sebenarnya tidak dipakai. Ini berarti layanan yang disuntikkan komponen - terutama Renderer2 bukanlah layanan yang sebenarnya Anda inginkan. Faktanya ketika saya mencoba @SkipSelf() private renderer2: Renderer2 itu akan melompat ke komponen dinamis terluar.

Jadi saya menyadari bahwa saya harus menghindari pembuatan portal komponen sampai komponen host sebenarnya sedang 'dijalankan':

Seperti inilah upaya aslinya (dengan portalInstances yang dibuat dengan penuh semangat):

    <ng-template [cdkPortalOutlet]="pagenode.portalInstance"></ng-template>

Lalu saya menyadari bahwa saya bisa membuat arahan portal sendiri untuk melakukan apa yang saya inginkan dan banyak lagi!

    <ng-template [dynamicComponentOutlet]="pagenode"></ng-template>

Perhatikan bagaimana saya meneruskan node dan bukan instance portal.

Jadi apa yang akan dilakukan arahan ini adalah:

  • Ambil pagenode yang telah diprarender yang mewakili definisi saja komponen dinamis (dan turunannya)
  • Pertama kali ia mencoba melampirkan portal, ia sebenarnya akan membuat instance ComponentPortal dengan Injector induk yang benar
  • Karena konteks injektor dari outlet dynamicComponentOutlet adalah komponen host, ia juga dapat menghasilkan dan menerapkan atribut _ngcontent-app-c338 (yang merupakan keseluruhan masalah yang dibahas dalam masalah ini!).

Inilah solusi saya:

  1. Pertama saya perlu membuat LazyComponentOutlet yang berisi placeholder untuk ComponentPortal dan juga data apa pun yang diperlukan untuk membuatnya. Saya baru saja menelepon ini params karena terserah Anda. Saya juga tidak menyertakan definisi ComponentPortalParams karena alasan yang sama. Minimal itu perlu menyertakan jenis komponen.
// 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 
}

Kemudian atribut DynamicComponentPortalHost (ganti namanya sesuka Anda):

Perhatikan bahwa ini terinspirasi oleh cara mereka melakukan pewarisan portal di 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!;
    }
}

Akhirnya metode ini bekerja dengan baik untuk satu item dan bukan untuk pohon. Atau Anda dapat mengekstrak hanya bagian yang merender atribut ngContent jika Anda tidak dapat memasukkannya ke dalam proyek yang sudah ada.

Dan itu saja! Tentu saja jika mereka memperbaiki (atau mengubah enkapsulasi) ini di masa mendatang, Anda hanya perlu memperbaruinya di satu tempat.

person Simon_Weaver    schedule 01.07.2021