หมายเหตุ: มีปัญหาที่เปิดอยู่ที่เกี่ยวข้องกับเรื่องนี้ แต่ เปิดมาเกือบ 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) ไม่ได้ทำเช่นนี้ ฉันคิดว่าเหตุผลคือหนึ่งในการใช้งานหลักสำหรับพอร์ทัลคือการนำส่วนประกอบที่กำหนดมาไว้ในที่เดียวและวางไว้บนเพจของคุณทุกที่ที่คุณต้องการ ดังนั้นในกรณีนั้น parent injector ก็ไม่สมเหตุสมผล
หากคุณกำลังสร้างส่วนประกอบแบบไดนามิกและวางไว้ในส่วนประกอบเดียวกัน คุณควรใช้ตัวสร้างต่อไปนี้:
const componentPortal = new ComponentPortal(component, null, parentInjector);
อย่างไรก็ตาม หากคุณกำลังสร้างแผนผังส่วนประกอบแบบไดนามิก สิ่งนี้จะกลายเป็นปัญหาด้านลอจิสติกส์! คุณต้องทำให้ส่วนประกอบโฮสต์ของคุณยุ่งเหยิงด้วยรหัส parentInjector ทั้งหมดนี้
วิธีแก้ปัญหาของฉันสำหรับปัญหา ViewEncapsulation.Emulated
แอปพลิเคชันของฉันคือ UI แบบกราฟิกเพื่อออกแบบเพจจากส่วนประกอบต่างๆ เช่น กริด ตาราง รูปภาพ วิดีโอ ฯลฯ
โมเดลนี้ถูกกำหนดให้เป็นแผนผังของ 'โหนดที่เรนเดอร์' ดังต่อไปนี้ อย่างที่คุณเห็นฉันมี 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
ได้ (ซึ่งเป็นปัญหาทั้งหมดที่เกี่ยวกับปัญหานี้!)
นี่คือวิธีแก้ปัญหาของฉัน:
- ก่อนอื่น ฉันต้องสร้าง
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
หากคุณไม่สามารถใส่สิ่งนี้ลงในโปรเจ็กต์ที่มีอยู่ได้
แค่นั้นแหละ! แน่นอนว่าหากพวกเขาแก้ไข (หรือเปลี่ยนแปลงการห่อหุ้ม) สิ่งนี้ในอนาคต คุณจะต้องอัปเดตสิ่งนี้ในที่เดียวเท่านั้น
person
Simon_Weaver
schedule
01.07.2021