From d8caf1706aad74509c9301ef02a01ce6e3ba02ca Mon Sep 17 00:00:00 2001 From: chintankavathia Date: Fri, 24 Apr 2026 17:11:15 +0530 Subject: [PATCH] feat(cdk/portal): add directives support to ComponentPortal Added a `directives` property to `ComponentPortal` that gets passed through to `createComponent` in both `DomPortalOutlet` and `CdkPortalOutlet`. This allows applying directives to dynamically created components via portals. Closes https://github.com/angular/components/issues/33141 --- goldens/cdk/dialog/index.api.md | 1 + goldens/cdk/menu/index.api.md | 1 + goldens/cdk/overlay/index.api.md | 1 + goldens/cdk/portal/index.api.md | 5 +++- src/cdk/portal/dom-portal-outlet.ts | 2 ++ src/cdk/portal/portal-directives.ts | 1 + src/cdk/portal/portal.spec.ts | 39 +++++++++++++++++++++++++++++ src/cdk/portal/portal.ts | 9 +++++++ 8 files changed, 58 insertions(+), 1 deletion(-) diff --git a/goldens/cdk/dialog/index.api.md b/goldens/cdk/dialog/index.api.md index 3a84074f0660..219116155434 100644 --- a/goldens/cdk/dialog/index.api.md +++ b/goldens/cdk/dialog/index.api.md @@ -10,6 +10,7 @@ import * as _angular_cdk_portal from '@angular/cdk/portal'; import { Binding } from '@angular/core'; import { ChangeDetectorRef } from '@angular/core'; import { ComponentRef } from '@angular/core'; +import { DirectiveWithBindings } from '@angular/core'; import { DoCheck } from '@angular/core'; import { ElementRef } from '@angular/core'; import { EmbeddedViewRef } from '@angular/core'; diff --git a/goldens/cdk/menu/index.api.md b/goldens/cdk/menu/index.api.md index c9cb1c05cc24..70679ebd4d90 100644 --- a/goldens/cdk/menu/index.api.md +++ b/goldens/cdk/menu/index.api.md @@ -7,6 +7,7 @@ import { AfterContentInit } from '@angular/core'; import { Binding } from '@angular/core'; import { ComponentRef } from '@angular/core'; +import { DirectiveWithBindings } from '@angular/core'; import { DoCheck } from '@angular/core'; import { ElementRef } from '@angular/core'; import { EmbeddedViewRef } from '@angular/core'; diff --git a/goldens/cdk/overlay/index.api.md b/goldens/cdk/overlay/index.api.md index 699cb0dd04a1..1b84e43d9ae3 100644 --- a/goldens/cdk/overlay/index.api.md +++ b/goldens/cdk/overlay/index.api.md @@ -7,6 +7,7 @@ import { AfterContentInit } from '@angular/core'; import { Binding } from '@angular/core'; import { ComponentRef } from '@angular/core'; +import { DirectiveWithBindings } from '@angular/core'; import { DoCheck } from '@angular/core'; import { ElementRef } from '@angular/core'; import { EmbeddedViewRef } from '@angular/core'; diff --git a/goldens/cdk/portal/index.api.md b/goldens/cdk/portal/index.api.md index 4f87dba62dfb..0d09a65c62f9 100644 --- a/goldens/cdk/portal/index.api.md +++ b/goldens/cdk/portal/index.api.md @@ -7,6 +7,7 @@ import { ApplicationRef } from '@angular/core'; import { Binding } from '@angular/core'; import { ComponentRef } from '@angular/core'; +import { DirectiveWithBindings } from '@angular/core'; import { ElementRef } from '@angular/core'; import { EmbeddedViewRef } from '@angular/core'; import { EventEmitter } from '@angular/core'; @@ -15,6 +16,7 @@ import { Injector } from '@angular/core'; import { OnDestroy } from '@angular/core'; import { OnInit } from '@angular/core'; import { TemplateRef } from '@angular/core'; +import { Type } from '@angular/core'; import { ViewContainerRef } from '@angular/core'; // @public @@ -72,9 +74,10 @@ export type CdkPortalOutletAttachedRef = ComponentRef | EmbeddedViewRef extends Portal> { - constructor(component: ComponentType, viewContainerRef?: ViewContainerRef | null, injector?: Injector | null, projectableNodes?: Node[][] | null, bindings?: Binding[]); + constructor(component: ComponentType, viewContainerRef?: ViewContainerRef | null, injector?: Injector | null, projectableNodes?: Node[][] | null, bindings?: Binding[], directives?: (Type | DirectiveWithBindings)[]); readonly bindings: Binding[] | null; component: ComponentType; + readonly directives: (Type | DirectiveWithBindings)[] | null; injector?: Injector | null; projectableNodes?: Node[][] | null; viewContainerRef?: ViewContainerRef | null; diff --git a/src/cdk/portal/dom-portal-outlet.ts b/src/cdk/portal/dom-portal-outlet.ts index 4bdc595ff10a..880f1eea5a35 100644 --- a/src/cdk/portal/dom-portal-outlet.ts +++ b/src/cdk/portal/dom-portal-outlet.ts @@ -60,6 +60,7 @@ export class DomPortalOutlet extends BasePortalOutlet { ngModuleRef, projectableNodes: portal.projectableNodes || undefined, bindings: portal.bindings || undefined, + directives: portal.directives || undefined, }); this.setDisposeFn(() => componentRef.destroy()); @@ -76,6 +77,7 @@ export class DomPortalOutlet extends BasePortalOutlet { environmentInjector, projectableNodes: portal.projectableNodes || undefined, bindings: portal.bindings || undefined, + directives: portal.directives || undefined, }); appRef.attachView(componentRef.hostView); diff --git a/src/cdk/portal/portal-directives.ts b/src/cdk/portal/portal-directives.ts index d234ed8f14d2..75a257e9e386 100644 --- a/src/cdk/portal/portal-directives.ts +++ b/src/cdk/portal/portal-directives.ts @@ -133,6 +133,7 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr projectableNodes: portal.projectableNodes || undefined, ngModuleRef: this._moduleRef || undefined, bindings: portal.bindings || undefined, + directives: portal.directives || undefined, }); // If we're using a view container that's different from the injected one (e.g. when the portal diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts index 63beb67f2528..e910307ba319 100644 --- a/src/cdk/portal/portal.spec.ts +++ b/src/cdk/portal/portal.spec.ts @@ -481,6 +481,20 @@ describe('Portals', () => { fixture.detectChanges(); expect(ref.instance.flavor).toBe('cheese'); }); + + it('should be able to pass directives to the component via a CdkPortalOutlet', () => { + const componentPortal = new ComponentPortal(PizzaMsg, null, null, null, undefined, [ + HighlightDirective, + ]); + + fixture.componentInstance.selectedPortal = componentPortal; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + const ref = fixture.componentInstance.portalOutlet.attachedRef as ComponentRef; + const hostElement = ref.location.nativeElement as HTMLElement; + expect(hostElement.style.background).toBe('yellow'); + }); }); describe('DomPortalOutlet', () => { @@ -750,6 +764,19 @@ describe('Portals', () => { someFixture.detectChanges(); expect(componentInstance.flavor).toBe('pepperoni'); }); + + it('should be able to pass directives to the component via a DomPortalOutlet', () => { + const portal = new ComponentPortal(PizzaMsg, null, null, null, undefined, [ + HighlightDirective, + ]); + + const ref = portal.attach(host); + someFixture.changeDetectorRef.markForCheck(); + someFixture.detectChanges(); + + const hostElement = ref.location.nativeElement as HTMLElement; + expect(hostElement.style.background).toBe('yellow'); + }); }); }); @@ -769,6 +796,18 @@ class ChocolateInjector { } } +/** Simple directive for testing directives support in ComponentPortal. */ +@Directive({ + selector: 'pizza-msg', +}) +class HighlightDirective { + private _elementRef = inject>(ElementRef); + + constructor() { + this._elementRef.nativeElement.style.background = 'yellow'; + } +} + /** Simple component for testing ComponentPortal. */ @Component({ selector: 'pizza-msg', diff --git a/src/cdk/portal/portal.ts b/src/cdk/portal/portal.ts index c1db210cf47b..1db5d8532f66 100644 --- a/src/cdk/portal/portal.ts +++ b/src/cdk/portal/portal.ts @@ -14,6 +14,8 @@ import { EmbeddedViewRef, Injector, Binding, + Type, + DirectiveWithBindings, } from '@angular/core'; import { throwNullPortalOutletError, @@ -105,12 +107,18 @@ export class ComponentPortal extends Portal> { */ readonly bindings: Binding[] | null; + /** + * Directives to apply to the created component. + */ + readonly directives: (Type | DirectiveWithBindings)[] | null; + constructor( component: ComponentType, viewContainerRef?: ViewContainerRef | null, injector?: Injector | null, projectableNodes?: Node[][] | null, bindings?: Binding[], + directives?: (Type | DirectiveWithBindings)[], ) { super(); this.component = component; @@ -118,6 +126,7 @@ export class ComponentPortal extends Portal> { this.injector = injector; this.projectableNodes = projectableNodes; this.bindings = bindings || null; + this.directives = directives || null; } }