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:
- 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