Skip to content

Commit 94d7b2e

Browse files
committed
fix(platform-browser): IsolatedShadowDom encapsulation
Implement IsolatedStyleScopeService. Refactor IsolatedShadowDom implementation to fix various bugs. Uses native slot instead of ng-content.
1 parent 8f3fdc3 commit 94d7b2e

File tree

22 files changed

+784
-134
lines changed

22 files changed

+784
-134
lines changed

adev/src/content/guide/routing/data-resolvers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,11 @@ export class App {
205205
map(event => {
206206
if (event instanceof NavigationError) {
207207
this.lastFailedUrl.set(event.url);
208-
208+
209209
if (event.error) {
210210
console.error('Navigation error', event.error)
211211
}
212-
212+
213213
return 'Navigation failed. Please try again.';
214214
}
215215
return '';

goldens/public-api/compiler-cli/error_code.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export enum ErrorCode {
7878
INLINE_TYPE_CTOR_REQUIRED = 8901,
7979
INTERPOLATED_SIGNAL_NOT_INVOKED = 8109,
8080
INVALID_BANANA_IN_BOX = 8101,
81+
ISOLATED_SHADOW_DOM_INVALID_CONTENT_PROJECTION = 2027,
8182
LET_USED_BEFORE_DEFINITION = 8016,
8283
LOCAL_COMPILATION_UNRESOLVED_CONST = 11001,
8384
LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION = 11003,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"dist/main.js": 108611,
3-
"dist/polyfills.js": 34169,
4-
"dist/lazy.routes-[hash].js": 361
2+
"dist/main.js": 113764,
3+
"dist/polyfills.js": 34585,
4+
"dist/lazy.routes-[hash].js": 348
55
}

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,24 @@ export class ComponentDecoratorHandler
908908
}
909909
}
910910

911+
// Check for ng-content in IsolatedShadowDom components
912+
if (encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom) {
913+
const contentNode = findContentNode(template.nodes);
914+
if (contentNode !== null) {
915+
if (diagnostics === undefined) {
916+
diagnostics = [];
917+
}
918+
diagnostics.push(
919+
makeDiagnostic(
920+
ErrorCode.ISOLATED_SHADOW_DOM_INVALID_CONTENT_PROJECTION,
921+
component.get('template') ?? node.name,
922+
`ng-content projection is not supported with ViewEncapsulation.ExperimentalIsolatedShadowDom. ` +
923+
`Use native <slot> elements instead. Content will remain in the light DOM and be projected via slots.`,
924+
),
925+
);
926+
}
927+
}
928+
911929
// If inline styles were preprocessed use those
912930
let inlineStyles: string[] | null = null;
913931
if (this.preanalyzeStylesCache.has(node)) {
@@ -2637,3 +2655,24 @@ function validateStandaloneImports(
26372655
function isDefaultImport(node: ts.ImportDeclaration): boolean {
26382656
return node.importClause !== undefined && node.importClause.namedBindings === undefined;
26392657
}
2658+
2659+
/**
2660+
* Recursively searches through template nodes to find a Content node (ng-content).
2661+
* Returns the first Content node found, or null if none exist.
2662+
*/
2663+
function findContentNode(nodes: any[]): any | null {
2664+
for (const node of nodes) {
2665+
// Check if this is a Content node (ng-content)
2666+
if (node.name === 'ng-content') {
2667+
return node;
2668+
}
2669+
// Recursively check children
2670+
if (node.children && node.children.length > 0) {
2671+
const found = findContentNode(node.children);
2672+
if (found !== null) {
2673+
return found;
2674+
}
2675+
}
2676+
}
2677+
return null;
2678+
}

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ export enum ErrorCode {
184184
*/
185185
COMPONENT_ANIMATIONS_CONFLICT = 2027,
186186

187+
/**
188+
* Raised when a component with `ViewEncapsulation.ExperimentalIsolatedShadowDom` uses `<ng-content>`.
189+
* ExperimentalIsolatedShadowDom components must use native `<slot>` elements instead.
190+
*/
191+
ISOLATED_SHADOW_DOM_INVALID_CONTENT_PROJECTION = 2028,
192+
187193
SYMBOL_NOT_EXPORTED = 3001,
188194
/**
189195
* Raised when a relationship between directives and/or pipes would cause a cyclic import to be

packages/core/src/core.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export {createComponent, reflectComponentType, ComponentMirror} from './render3/
105105
export {isStandalone} from './render3/def_getters';
106106
export {AfterRenderRef} from './render3/after_render/api';
107107
export {publishExternalGlobalUtil as ɵpublishExternalGlobalUtil} from './render3/util/global_utils';
108+
export {getLContext as ɵgetLContext} from './render3/context_discovery';
109+
export {unwrapRNode as ɵunwrapRNode} from './render3/util/view_utils';
110+
export {HOST as ɵHOST, PARENT as ɵPARENT} from './render3/interfaces/view';
108111
export {enableProfiling} from './render3/debug/chrome_dev_tools_performance';
109112
export {
110113
AfterRenderOptions,
@@ -126,6 +129,7 @@ export {
126129
} from './animation/interfaces';
127130

128131
import {global} from './util/global';
132+
129133
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
130134
// This helper is to give a reasonable error message to people upgrading to v9 that have not yet
131135
// installed `@angular/localize` in their app.

packages/core/src/render3/instructions/projection.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {findMatchingDehydratedView} from '../../hydration/views';
99
import {isDetachedByI18n} from '../../i18n/utils';
1010
import {newArray} from '../../util/array_utils';
1111
import {assertLContainer, assertTNode} from '../assert';
12-
import {ComponentTemplate} from '../interfaces/definition';
12+
import {ComponentDef, ComponentTemplate} from '../interfaces/definition';
1313
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
14+
import {ViewEncapsulation} from '../../metadata/view';
1415
import {ProjectionSlots} from '../interfaces/projection';
1516
import {
1617
DECLARATION_COMPONENT_VIEW,
@@ -94,7 +95,35 @@ export function matchingProjectionSlotIndex(
9495
* @codeGenApi
9596
*/
9697
export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
97-
const componentNode = getLView()[DECLARATION_COMPONENT_VIEW][T_HOST] as TElementNode;
98+
const lView = getLView();
99+
const declarationComponentView = lView[DECLARATION_COMPONENT_VIEW];
100+
const componentNode = declarationComponentView[T_HOST] as TElementNode;
101+
102+
// Check if this is an IsolatedShadowDom component
103+
// The component instance is stored in CONTEXT
104+
const CONTEXT = 8; // CONTEXT constant
105+
const componentInstance = declarationComponentView[CONTEXT];
106+
107+
// Get the component definition from the constructor
108+
let componentDef: ComponentDef<any> | undefined;
109+
if (componentInstance?.constructor) {
110+
// Angular stores component definitions on the constructor with ɵcmp property
111+
componentDef = (componentInstance.constructor as any).ɵcmp;
112+
}
113+
114+
if (
115+
componentDef &&
116+
componentDef.encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom
117+
) {
118+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
119+
throw new Error(
120+
`ng-content projection is not supported with ViewEncapsulation.IsolatedShadowDom. ` +
121+
`Use native <slot> elements instead. Content will remain in the light DOM and be projected via slots.`,
122+
);
123+
}
124+
// Don't setup projection for IsolatedShadowDom
125+
return;
126+
}
98127

99128
if (!componentNode.projection) {
100129
// If no explicit projection slots are defined, fall back to a single

packages/core/test/acceptance/renderer_factory_spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ɵDomRendererFactory2 as DomRendererFactory2,
2424
EventManager,
2525
ɵSharedStylesHost,
26+
ɵStyleScopeService as StyleScopeService,
2627
} from '@angular/platform-browser';
2728
import {isBrowser, isNode} from '@angular/private/testing';
2829
import {expect} from '@angular/private/testing/matchers';
@@ -396,6 +397,7 @@ function getRendererFactory2(document: Document): RendererFactory2 {
396397
const fakeNgZone: NgZone = new NoopNgZone();
397398
const eventManager = new EventManager([], fakeNgZone);
398399
const appId = 'app-id';
400+
const styleScopeService = new StyleScopeService();
399401
const rendererFactory = new DomRendererFactory2(
400402
eventManager,
401403
new ɵSharedStylesHost(document, appId),
@@ -404,6 +406,8 @@ function getRendererFactory2(document: Document): RendererFactory2 {
404406
document,
405407
fakeNgZone,
406408
null,
409+
null, // tracingService
410+
styleScopeService,
407411
);
408412
const origCreateRenderer = rendererFactory.createRenderer;
409413
rendererFactory.createRenderer = function (element: any, type: RendererType2 | null) {

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@
132132
"InjectionToken",
133133
"Injector",
134134
"InputFlags",
135+
"IsolatedStyleScopeService",
135136
"KeyEventsPlugin",
137+
"LContext",
136138
"LEAVE_CLASSNAME",
137139
"LEAVE_TOKEN",
138140
"LEAVE_TOKEN_REGEX",
@@ -397,6 +399,7 @@
397399
"createHostElement",
398400
"createInjector",
399401
"createInjectorWithoutInjectorInstances",
402+
"createLContext",
400403
"createLFrame",
401404
"createLView",
402405
"createLinkElement",
@@ -468,6 +471,9 @@
468471
"finalizeConsumerAfterComputation",
469472
"findAttrIndexInNode",
470473
"findDirectiveDefMatches",
474+
"findViaComponent",
475+
"findViaDirective",
476+
"findViaNativeElement",
471477
"flattenGroupPlayers",
472478
"flattenStyles",
473479
"forEachSingleProvider",
@@ -494,6 +500,7 @@
494500
"getDOM",
495501
"getDeclarationTNode",
496502
"getDirectiveDef",
503+
"getDirectivesAtNodeIndex",
497504
"getElementDepthCount",
498505
"getFactoryDef",
499506
"getFirstLContainer",
@@ -507,7 +514,9 @@
507514
"getInjectorIndex",
508515
"getInsertInFrontOfRNode",
509516
"getInsertInFrontOfRNodeWithNoI18n",
517+
"getLContext",
510518
"getLView",
519+
"getLViewById",
511520
"getLViewParent",
512521
"getNameOnlyMarkerIndex",
513522
"getNamespace",
@@ -607,12 +616,14 @@
607616
"isBoundToModule",
608617
"isComponentDef",
609618
"isComponentHost",
619+
"isComponentInstance",
610620
"isContentQueryHost",
611621
"isCssClassMatching",
612622
"isCurrentTNodeParent",
613623
"isDestroyed",
614624
"isDetachedByI18n",
615625
"isDirectiveHost",
626+
"isDirectiveInstance",
616627
"isElementNode",
617628
"isEnvironmentProviders",
618629
"isExistingProvider",
@@ -742,6 +753,7 @@
742753
"providerToRecord",
743754
"publishSignalConfiguration",
744755
"queueEnterAnimations",
756+
"readPatchedData",
745757
"refreshContentQueries",
746758
"refreshView",
747759
"registerFailed",
@@ -835,6 +847,7 @@
835847
"trackMovedView",
836848
"transition",
837849
"transitionFailed",
850+
"traverseNextElement",
838851
"trigger",
839852
"triggerBuildFailed",
840853
"triggerTransitionsFailed",

packages/core/test/bundling/create_component/bundle.golden_symbols.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@
9898
"InjectionToken",
9999
"Injector",
100100
"InputFlags",
101+
"IsolatedStyleScopeService",
101102
"KeyEventsPlugin",
103+
"LContext",
102104
"LOCALE_ID",
103105
"LOCALE_ID",
104106
"MATH_ML_NAMESPACE",
@@ -309,6 +311,7 @@
309311
"createInjectorWithoutInjectorInstances",
310312
"createInputSignal",
311313
"createLContainer",
314+
"createLContext",
312315
"createLFrame",
313316
"createLView",
314317
"createLinkElement",
@@ -369,6 +372,9 @@
369372
"extractDirectiveDef",
370373
"finalizeConsumerAfterComputation",
371374
"findMatchingDehydratedView",
375+
"findViaComponent",
376+
"findViaDirective",
377+
"findViaNativeElement",
372378
"forEachSingleProvider",
373379
"forkInnerZoneWithAngularBehavior",
374380
"formatRuntimeError",
@@ -392,6 +398,7 @@
392398
"getDOM",
393399
"getDeclarationTNode",
394400
"getDirectiveDef",
401+
"getDirectivesAtNodeIndex",
395402
"getFactoryDef",
396403
"getFirstLContainer",
397404
"getFirstNativeNode",
@@ -405,7 +412,9 @@
405412
"getInjectorIndex",
406413
"getInsertInFrontOfRNode",
407414
"getInsertInFrontOfRNodeWithNoI18n",
415+
"getLContext",
408416
"getLView",
417+
"getLViewById",
409418
"getLViewParent",
410419
"getNativeByIndex",
411420
"getNativeByTNode",
@@ -490,10 +499,12 @@
490499
"isBoundToModule",
491500
"isComponentDef",
492501
"isComponentHost",
502+
"isComponentInstance",
493503
"isContentQueryHost",
494504
"isCurrentTNodeParent",
495505
"isDestroyed",
496506
"isDetachedByI18n",
507+
"isDirectiveInstance",
497508
"isEnvironmentProviders",
498509
"isExistingProvider",
499510
"isFactoryProvider",
@@ -588,6 +599,7 @@
588599
"providerToRecord",
589600
"publishSignalConfiguration",
590601
"queueEnterAnimations",
602+
"readPatchedData",
591603
"refreshContentQueries",
592604
"refreshView",
593605
"registerHostBindingOpCodes",
@@ -669,6 +681,7 @@
669681
"toInputRefArray",
670682
"toOutputRefArray",
671683
"trackMovedView",
684+
"traverseNextElement",
672685
"uniqueIdCounter",
673686
"unregisterLView",
674687
"unwrapRNode",

0 commit comments

Comments
 (0)