@@ -10,6 +10,7 @@ import {RuntimeError, RuntimeErrorCode} from '../../errors';
1010import { Writable } from '../../interface/type' ;
1111import { DoCheck , OnChanges , OnInit } from '../../interface/lifecycle_hooks' ;
1212import {
13+ assertDefined ,
1314 assertGreaterThan ,
1415 assertGreaterThanOrEqual ,
1516 assertNotEqual ,
@@ -39,7 +40,7 @@ import {
3940 type TElementNode ,
4041 type TNode ,
4142} from '../interfaces/node' ;
42- import { isComponentDef , isComponentHost } from '../interfaces/type_checks' ;
43+ import { isComponentDef } from '../interfaces/type_checks' ;
4344import { HEADER_OFFSET , HostBindingOpCodes , type LView , type TView } from '../interfaces/view' ;
4445import { isInlineTemplate } from '../node_selector_matcher' ;
4546import { NO_CHANGE } from '../tokens' ;
@@ -51,6 +52,13 @@ export type DirectiveMatcherStrategy = (
5152 tNode : TElementNode | TContainerNode | TElementContainerNode ,
5253) => DirectiveDef < unknown > [ ] | null ;
5354
55+ /**
56+ * Map that tracks a selector-matched directive to the range within which its host directives
57+ * are declared. Host directives for a specific directive are always contiguous within the runtime.
58+ * Note that both the start and end are inclusive and they're both **after** `tNode.directiveStart`.
59+ */
60+ type HostDirectiveRanges = Map < DirectiveDef < unknown > , [ start : number , end : number ] > ;
61+
5462/**
5563 * Resolve the matched directives on a node.
5664 */
@@ -61,21 +69,28 @@ export function resolveDirectives(
6169 localRefs : string [ ] | null ,
6270 directiveMatcher : DirectiveMatcherStrategy ,
6371) : void {
64- // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in
65- // tsickle.
72+ // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in tsickle.
6673 ngDevMode && assertFirstCreatePass ( tView ) ;
6774
6875 const exportsMap : Record < string , number > | null = localRefs === null ? null : { '' : - 1 } ;
6976 const matchedDirectiveDefs = directiveMatcher ( tView , tNode ) ;
7077
7178 if ( matchedDirectiveDefs !== null ) {
72- const [ directiveDefs , hostDirectiveDefs ] = resolveHostDirectives (
79+ const [ directiveDefs , hostDirectiveDefs , hostDirectiveRanges ] = resolveHostDirectives (
7380 tView ,
7481 tNode ,
7582 matchedDirectiveDefs ,
7683 ) ;
7784
78- initializeDirectives ( tView , lView , tNode , directiveDefs , exportsMap , hostDirectiveDefs ) ;
85+ initializeDirectives (
86+ tView ,
87+ lView ,
88+ tNode ,
89+ directiveDefs ,
90+ exportsMap ,
91+ hostDirectiveDefs ,
92+ hostDirectiveRanges ,
93+ ) ;
7994 }
8095 if ( exportsMap !== null && localRefs !== null ) {
8196 cacheMatchingLocalNames ( tNode , localRefs , exportsMap ) ;
@@ -104,38 +119,51 @@ function cacheMatchingLocalNames(
104119 }
105120}
106121
107- export function resolveHostDirectives (
122+ function resolveHostDirectives (
108123 tView : TView ,
109124 tNode : TNode ,
110125 matches : DirectiveDef < unknown > [ ] ,
111- ) : [ matches : DirectiveDef < unknown > [ ] , hostDirectiveDefs : HostDirectiveDefs | null ] {
126+ ) : [
127+ matches : DirectiveDef < unknown > [ ] ,
128+ hostDirectiveDefs : HostDirectiveDefs | null ,
129+ hostDirectiveRanges : HostDirectiveRanges | null ,
130+ ] {
112131 const allDirectiveDefs : DirectiveDef < unknown > [ ] = [ ] ;
132+ const hasComponent = matches . length > 0 && isComponentDef ( matches [ 0 ] ) ;
113133 let hostDirectiveDefs : HostDirectiveDefs | null = null ;
114-
115- for ( const def of matches ) {
134+ let hostDirectiveRanges : HostDirectiveRanges | null = null ;
135+
136+ // Components are inserted at the front of the matches array so that their lifecycle
137+ // hooks run before any directive lifecycle hooks. This appears to be for ViewEngine
138+ // compatibility. This logic doesn't make sense with host directives, because it
139+ // would allow the host directives to undo any overrides the host may have made.
140+ // To handle this case, the host directives of components are inserted at the beginning
141+ // of the array, followed by the component. As such, the insertion order is as follows:
142+ // 1. Host directives belonging to the selector-matched component.
143+ // 2. Selector-matched component.
144+ // 3. Host directives belonging to selector-matched directives.
145+ // 4. Selector-matched dir
146+ if ( hasComponent ) {
147+ const def = matches [ 0 ] ;
116148 if ( def . findHostDirectiveDefs !== null ) {
117- // TODO(pk): probably could return matches instead of taking in an array to fill in?
149+ hostDirectiveRanges ??= new Map ( ) ;
118150 hostDirectiveDefs ??= new Map ( ) ;
119- // Components are inserted at the front of the matches array so that their lifecycle
120- // hooks run before any directive lifecycle hooks. This appears to be for ViewEngine
121- // compatibility. This logic doesn't make sense with host directives, because it
122- // would allow the host directives to undo any overrides the host may have made.
123- // To handle this case, the host directives of components are inserted at the beginning
124- // of the array, followed by the component. As such, the insertion order is as follows:
125- // 1. Host directives belonging to the selector-matched component.
126- // 2. Selector-matched component.
127- // 3. Host directives belonging to selector-matched directives.
128- // 4. Selector-matched directives.
129- def . findHostDirectiveDefs ( def , allDirectiveDefs , hostDirectiveDefs ) ;
151+ resolveHostDirectivesForDef ( def , allDirectiveDefs , hostDirectiveRanges , hostDirectiveDefs ) ;
130152 }
153+ markAsComponentHost ( tView , tNode , allDirectiveDefs . push ( def ) - 1 ) ;
154+ }
131155
132- if ( isComponentDef ( def ) ) {
133- allDirectiveDefs . push ( def ) ;
134- markAsComponentHost ( tView , tNode , allDirectiveDefs . length - 1 ) ;
156+ // If there's a component, we already processed it above so we can skip it here.
157+ for ( let i = hasComponent ? 1 : 0 ; i < matches . length ; i ++ ) {
158+ const def = matches [ i ] ;
159+ if ( def . findHostDirectiveDefs !== null ) {
160+ hostDirectiveRanges ??= new Map ( ) ;
161+ hostDirectiveDefs ??= new Map ( ) ;
162+ resolveHostDirectivesForDef ( def , allDirectiveDefs , hostDirectiveRanges , hostDirectiveDefs ) ;
135163 }
136164 }
137165
138- if ( isComponentHost ( tNode ) ) {
166+ if ( hasComponent ) {
139167 allDirectiveDefs . push ( ...matches . slice ( 1 ) ) ;
140168 } else {
141169 allDirectiveDefs . push ( ...matches ) ;
@@ -145,7 +173,23 @@ export function resolveHostDirectives(
145173 assertNoDuplicateDirectives ( allDirectiveDefs ) ;
146174 }
147175
148- return [ allDirectiveDefs , hostDirectiveDefs ] ;
176+ return [ allDirectiveDefs , hostDirectiveDefs , hostDirectiveRanges ] ;
177+ }
178+
179+ function resolveHostDirectivesForDef (
180+ def : DirectiveDef < unknown > ,
181+ allDirectiveDefs : DirectiveDef < unknown > [ ] ,
182+ hostDirectiveRanges : HostDirectiveRanges ,
183+ hostDirectiveDefs : HostDirectiveDefs ,
184+ ) {
185+ ngDevMode && assertDefined ( def . findHostDirectiveDefs , 'Expected host directive resolve function' ) ;
186+ const start = allDirectiveDefs . length ;
187+ // TODO(pk): probably could return matches instead of taking in an array to fill in?
188+ def . findHostDirectiveDefs ! ( def , allDirectiveDefs , hostDirectiveDefs ) ;
189+
190+ // Note that these indexes are within the offset by `directiveStart`. We can't do the
191+ // offsetting here, because `directiveStart` hasn't been initialized on the TNode yet.
192+ hostDirectiveRanges . set ( def , [ start , allDirectiveDefs . length - 1 ] ) ;
149193}
150194
151195/**
@@ -168,38 +212,46 @@ function initializeDirectives(
168212 directives : DirectiveDef < unknown > [ ] ,
169213 exportsMap : { [ key : string ] : number } | null ,
170214 hostDirectiveDefs : HostDirectiveDefs | null ,
215+ hostDirectiveRanges : HostDirectiveRanges | null ,
171216) {
172217 ngDevMode && assertFirstCreatePass ( tView ) ;
173218
219+ const directivesLength = directives . length ;
220+
174221 // Publishes the directive types to DI so they can be injected. Needs to
175222 // happen in a separate pass before the TNode flags have been initialized.
176- for ( let i = 0 ; i < directives . length ; i ++ ) {
223+ for ( let i = 0 ; i < directivesLength ; i ++ ) {
177224 diPublicInInjector ( getOrCreateNodeInjectorForNode ( tNode , lView ) , tView , directives [ i ] . type ) ;
178225 }
179226
180- initTNodeFlags ( tNode , tView . data . length , directives . length ) ;
227+ initTNodeFlags ( tNode , tView . data . length , directivesLength ) ;
181228
182229 // When the same token is provided by several directives on the same node, some rules apply in
183230 // the viewEngine:
184231 // - viewProviders have priority over providers
185232 // - the last directive in NgModule.declarations has priority over the previous one
186233 // So to match these rules, the order in which providers are added in the arrays is very
187234 // important.
188- for ( let i = 0 ; i < directives . length ; i ++ ) {
235+ for ( let i = 0 ; i < directivesLength ; i ++ ) {
189236 const def = directives [ i ] ;
190237 if ( def . providersResolver ) def . providersResolver ( def ) ;
191238 }
192239 let preOrderHooksFound = false ;
193240 let preOrderCheckHooksFound = false ;
194- let directiveIdx = allocExpando ( tView , lView , directives . length , null ) ;
241+ let directiveIdx = allocExpando ( tView , lView , directivesLength , null ) ;
195242 ngDevMode &&
196243 assertSame (
197244 directiveIdx ,
198245 tNode . directiveStart ,
199246 'TNode.directiveStart should point to just allocated space' ,
200247 ) ;
201248
202- for ( let i = 0 ; i < directives . length ; i ++ ) {
249+ // If there's at least one directive, we'll have to track it so initialize the map.
250+ if ( directivesLength > 0 ) {
251+ tNode . directiveToIndex = new Map ( ) ;
252+ }
253+
254+ for ( let i = 0 ; i < directivesLength ; i ++ ) {
203255 const def = directives [ i ] ;
204256 // Merge the attrs in the order of matches. This assumes that the first directive is the
205257 // component itself, so that the component has the least priority.
@@ -208,6 +260,20 @@ function initializeDirectives(
208260 configureViewWithDirective ( tView , tNode , lView , directiveIdx , def ) ;
209261 saveNameToExportMap ( directiveIdx , def , exportsMap ) ;
210262
263+ // If a directive has host directives, we need to track both its index and the range within
264+ // the host directives are declared. Host directives are not tracked, but should be resolved
265+ // by looking up the host and getting its indexes from there.
266+ if ( hostDirectiveRanges !== null && hostDirectiveRanges . has ( def ) ) {
267+ const [ start , end ] = hostDirectiveRanges . get ( def ) ! ;
268+ tNode . directiveToIndex ! . set ( def . type , [
269+ directiveIdx ,
270+ start + tNode . directiveStart ,
271+ end + tNode . directiveStart ,
272+ ] ) ;
273+ } else if ( hostDirectiveDefs === null || ! hostDirectiveDefs . has ( def ) ) {
274+ tNode . directiveToIndex ! . set ( def . type , directiveIdx ) ;
275+ }
276+
211277 if ( def . contentQueries !== null ) tNode . flags |= TNodeFlags . hasContentQuery ;
212278 if ( def . hostBindings !== null || def . hostAttrs !== null || def . hostVars !== 0 )
213279 tNode . flags |= TNodeFlags . hasHostBindings ;
0 commit comments