Skip to content

Commit d767e0b

Browse files
JoostKjasonaden
authored andcommitted
fix(ivy): consider providers for view/content queries (angular#27156)
In ViewEngine it is possible to query for a token that is provided by a directive that is in scope. See StackBlitz for example: https://stackblitz.com/edit/ng-viewengine-viewchild-providers Material uses this pattern with its `MatFormFieldControl` setup, see https://bit.ly/2zgCUxD for documentation. PR Close angular#27156
1 parent 1db53da commit d767e0b

File tree

8 files changed

+170
-44
lines changed

8 files changed

+170
-44
lines changed

packages/core/src/render3/di.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,6 @@ function searchTokensOnInjector<T>(
396396
previousTView: TView | null) {
397397
const currentTView = injectorView[TVIEW];
398398
const tNode = currentTView.data[injectorIndex + TNODE] as TNode;
399-
const nodeFlags = tNode.flags;
400-
const nodeProviderIndexes = tNode.providerIndexes;
401-
const tInjectables = currentTView.data;
402399
// First, we step through providers
403400
let canAccessViewProviders = false;
404401
// We need to determine if view providers can be accessed by the starting element.
@@ -412,9 +409,35 @@ function searchTokensOnInjector<T>(
412409
// and check the host node of the current view to identify component views.
413410
if (previousTView == null && isComponent(tNode) && includeViewProviders ||
414411
previousTView != null && previousTView != currentTView &&
415-
(currentTView.node == null || currentTView.node !.type === TNodeType.Element)) {
412+
(currentTView.node == null || currentTView.node.type === TNodeType.Element)) {
416413
canAccessViewProviders = true;
417414
}
415+
const injectableIdx =
416+
locateDirectiveOrProvider(tNode, injectorView, token, canAccessViewProviders);
417+
if (injectableIdx !== null) {
418+
return getNodeInjectable(currentTView.data, injectorView, injectableIdx, tNode as TElementNode);
419+
} else {
420+
return NOT_FOUND;
421+
}
422+
}
423+
424+
/**
425+
* Searches for the given token among the node's directives and providers.
426+
*
427+
* @param tNode TNode on which directives are present.
428+
* @param view The view we are currently processing
429+
* @param token Provider token or type of a directive to look for.
430+
* @param canAccessViewProviders Whether view providers should be considered.
431+
* @returns Index of a found directive or provider, or null when none found.
432+
*/
433+
export function locateDirectiveOrProvider<T>(
434+
tNode: TNode, view: LViewData, token: Type<T>| InjectionToken<T>,
435+
canAccessViewProviders: boolean): number|null {
436+
const tView = view[TVIEW];
437+
const nodeFlags = tNode.flags;
438+
const nodeProviderIndexes = tNode.providerIndexes;
439+
const tInjectables = tView.data;
440+
418441
const startInjectables = nodeProviderIndexes & TNodeProviderIndexes.ProvidersStartIndexMask;
419442
const startDirectives = nodeFlags >> TNodeFlags.DirectiveStartingIndexShift;
420443
const cptViewProvidersCount =
@@ -426,10 +449,10 @@ function searchTokensOnInjector<T>(
426449
const providerTokenOrDef = tInjectables[i] as InjectionToken<any>| Type<any>| DirectiveDef<any>;
427450
if (i < startDirectives && token === providerTokenOrDef ||
428451
i >= startDirectives && (providerTokenOrDef as DirectiveDef<any>).type === token) {
429-
return getNodeInjectable(tInjectables, injectorView, i, tNode as TElementNode);
452+
return i;
430453
}
431454
}
432-
return NOT_FOUND;
455+
return null;
433456
}
434457

435458
/**

packages/core/src/render3/query.ts

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import {Type} from '../type';
1818
import {getSymbolIterator} from '../util';
1919

2020
import {assertDefined, assertEqual} from './assert';
21+
import {getNodeInjectable, locateDirectiveOrProvider} from './di';
2122
import {NG_ELEMENT_ID} from './fields';
2223
import {store, storeCleanupWithContext} from './instructions';
23-
import {DirectiveDef, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition';
24+
import {unusedValueExportToPlacateAjd as unused1} from './interfaces/definition';
2425
import {unusedValueExportToPlacateAjd as unused2} from './interfaces/injector';
25-
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused3} from './interfaces/node';
26+
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, unusedValueExportToPlacateAjd as unused3} from './interfaces/node';
2627
import {LQueries, unusedValueExportToPlacateAjd as unused4} from './interfaces/query';
2728
import {LViewData, TVIEW} from './interfaces/view';
2829
import {assertPreviousIsParent, getOrCreateCurrentQueries, getViewData} from './state';
@@ -242,41 +243,17 @@ function getIdxOfMatchingSelector(tNode: TNode, selector: string): number|null {
242243
return null;
243244
}
244245

245-
/**
246-
* Iterates over all the directives for a node and returns index of a directive for a given type.
247-
*
248-
* @param tNode TNode on which directives are present.
249-
* @param currentView The view we are currently processing
250-
* @param type Type of a directive to look for.
251-
* @returns Index of a found directive or null when none found.
252-
*/
253-
function getIdxOfMatchingDirective(tNode: TNode, currentView: LViewData, type: Type<any>): number|
254-
null {
255-
const defs = currentView[TVIEW].data;
256-
if (defs) {
257-
const flags = tNode.flags;
258-
const count = flags & TNodeFlags.DirectiveCountMask;
259-
const start = flags >> TNodeFlags.DirectiveStartingIndexShift;
260-
const end = start + count;
261-
for (let i = start; i < end; i++) {
262-
const def = defs[i] as DirectiveDef<any>;
263-
if (def.type === type) {
264-
return i;
265-
}
266-
}
267-
}
268-
return null;
269-
}
270246

271247
// TODO: "read" should be an AbstractType (FW-486)
272248
function queryByReadToken(read: any, tNode: TNode, currentView: LViewData): any {
273249
const factoryFn = (read as any)[NG_ELEMENT_ID];
274250
if (typeof factoryFn === 'function') {
275251
return factoryFn();
276252
} else {
277-
const matchingIdx = getIdxOfMatchingDirective(tNode, currentView, read as Type<any>);
253+
const matchingIdx = locateDirectiveOrProvider(tNode, currentView, read as Type<any>, false);
278254
if (matchingIdx !== null) {
279-
return currentView[matchingIdx];
255+
return getNodeInjectable(
256+
currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
280257
}
281258
}
282259
return null;
@@ -307,7 +284,8 @@ function queryRead(tNode: TNode, currentView: LViewData, read: any, matchingIdx:
307284
return queryByReadToken(read, tNode, currentView);
308285
}
309286
if (matchingIdx > -1) {
310-
return currentView[matchingIdx];
287+
return getNodeInjectable(
288+
currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
311289
}
312290
// if read token and / or strategy is not specified,
313291
// detect it using appropriate tNode type
@@ -326,7 +304,7 @@ function add(
326304
if (type === ViewEngine_TemplateRef) {
327305
result = queryByTemplateRef(type, tNode, currentView, predicate.read);
328306
} else {
329-
const matchingIdx = getIdxOfMatchingDirective(tNode, currentView, type);
307+
const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false);
330308
if (matchingIdx !== null) {
331309
result = queryRead(tNode, currentView, predicate.read, matchingIdx);
332310
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,9 @@
929929
{
930930
"name": "loadContext"
931931
},
932+
{
933+
"name": "locateDirectiveOrProvider"
934+
},
932935
{
933936
"name": "locateHostElement"
934937
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,9 @@
11001100
{
11011101
"name": "leaveView"
11021102
},
1103+
{
1104+
"name": "locateDirectiveOrProvider"
1105+
},
11031106
{
11041107
"name": "locateHostElement"
11051108
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,9 @@
947947
{
948948
"name": "loadInternal"
949949
},
950+
{
951+
"name": "locateDirectiveOrProvider"
952+
},
950953
{
951954
"name": "locateHostElement"
952955
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2198,6 +2198,9 @@
21982198
{
21992199
"name": "localeEn"
22002200
},
2201+
{
2202+
"name": "locateDirectiveOrProvider"
2203+
},
22012204
{
22022205
"name": "locateHostElement"
22032206
},

packages/core/test/render3/providers_spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,7 @@ function expectProvidersScenario(defs: {
12981298
}
12991299

13001300
class ViewChildDirective {
1301-
static ngComponentDef = defineDirective({
1301+
static ngDirectiveDef = defineDirective({
13021302
type: ViewChildDirective,
13031303
selectors: [['view-child']],
13041304
factory: () => testDirectiveInjection(defs.viewChild, new ViewChildDirective()),
@@ -1325,7 +1325,7 @@ function expectProvidersScenario(defs: {
13251325
}
13261326

13271327
class ContentChildDirective {
1328-
static ngComponentDef = defineDirective({
1328+
static ngDirectiveDef = defineDirective({
13291329
type: ContentChildDirective,
13301330
selectors: [['content-child']],
13311331
factory: () => testDirectiveInjection(defs.contentChild, new ContentChildDirective()),
@@ -1353,7 +1353,7 @@ function expectProvidersScenario(defs: {
13531353
}
13541354

13551355
class ParentDirective {
1356-
static ngComponentDef = defineDirective({
1356+
static ngDirectiveDef = defineDirective({
13571357
type: ParentDirective,
13581358
selectors: [['parent']],
13591359
factory: () => testDirectiveInjection(defs.parent, new ParentDirective()),
@@ -1362,7 +1362,7 @@ function expectProvidersScenario(defs: {
13621362
}
13631363

13641364
class ParentDirective2 {
1365-
static ngComponentDef = defineDirective({
1365+
static ngDirectiveDef = defineDirective({
13661366
type: ParentDirective2,
13671367
selectors: [['parent']],
13681368
factory: () => testDirectiveInjection(defs.parent, new ParentDirective2()),

packages/core/test/render3/query_spec.ts

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {NgForOfContext} from '@angular/common';
1010
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
1111

1212
import {EventEmitter} from '../..';
13-
14-
import {AttributeMarker, QueryList, defineComponent, defineDirective, detectChanges} from '../../src/render3/index';
13+
import {AttributeMarker, ProvidersFeature, QueryList, defineComponent, defineDirective, detectChanges} from '../../src/render3/index';
1514
import {getNativeByIndex} from '../../src/render3/util';
15+
1616
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, loadQueryList, reference, registerContentQuery, template, text} from '../../src/render3/instructions';
1717
import {RenderFlags} from '../../src/render3/interfaces/definition';
1818
import {query, queryRefresh} from '../../src/render3/query';
@@ -205,6 +205,119 @@ describe('query', () => {
205205
});
206206
});
207207

208+
describe('providers', () => {
209+
210+
class Service {}
211+
class Alias {}
212+
213+
let directive: MyDirective|null = null;
214+
215+
class MyDirective {
216+
constructor(public service: Service) {}
217+
218+
static ngDirectiveDef = defineDirective({
219+
type: MyDirective,
220+
selectors: [['', 'myDir', '']],
221+
factory: function MyDirective_Factory() {
222+
return directive = new MyDirective(directiveInject(Service));
223+
},
224+
features: [ProvidersFeature([Service, {provide: Alias, useExisting: Service}])],
225+
});
226+
}
227+
228+
beforeEach(() => directive = null);
229+
230+
// https://stackblitz.com/edit/ng-viewengine-viewchild-providers?file=src%2Fapp%2Fapp.component.ts
231+
it('should query for providers that are present on a directive', () => {
232+
233+
/**
234+
* <div myDir></div>
235+
* class App {
236+
* @ViewChild(MyDirective) directive: MyDirective;
237+
* @ViewChild(Service) service: Service;
238+
* @ViewChild(Alias) alias: Alias;
239+
* }
240+
*/
241+
class App {
242+
directive?: MyDirective;
243+
service?: Service;
244+
alias?: Alias;
245+
246+
static ngComponentDef = defineComponent({
247+
type: App,
248+
selectors: [['app']],
249+
consts: 4,
250+
vars: 0,
251+
factory: function App_Factory() { return new App(); },
252+
template: function App_Template(rf: RenderFlags, ctx: App) {
253+
if (rf & RenderFlags.Create) {
254+
element(3, 'div', ['myDir']);
255+
}
256+
},
257+
viewQuery: function(rf: RenderFlags, ctx: App) {
258+
let tmp: any;
259+
if (rf & RenderFlags.Create) {
260+
query(0, MyDirective, false);
261+
query(1, Service, false);
262+
query(2, Alias, false);
263+
}
264+
if (rf & RenderFlags.Update) {
265+
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.directive = tmp.first);
266+
queryRefresh(tmp = load<QueryList<any>>(1)) && (ctx.service = tmp.first);
267+
queryRefresh(tmp = load<QueryList<any>>(2)) && (ctx.alias = tmp.first);
268+
}
269+
},
270+
directives: [MyDirective]
271+
});
272+
}
273+
274+
const componentFixture = new ComponentFixture(App);
275+
expect(componentFixture.component.directive).toBe(directive !);
276+
expect(componentFixture.component.service).toBe(directive !.service);
277+
expect(componentFixture.component.alias).toBe(directive !.service);
278+
});
279+
280+
it('should resolve a provider if given as read token', () => {
281+
282+
/**
283+
* <div myDir></div>
284+
* class App {
285+
* @ViewChild(MyDirective, {read: Alias}}) service: Service;
286+
* }
287+
*/
288+
class App {
289+
service?: Service;
290+
291+
static ngComponentDef = defineComponent({
292+
type: App,
293+
selectors: [['app']],
294+
consts: 2,
295+
vars: 0,
296+
factory: function App_Factory() { return new App(); },
297+
template: function App_Template(rf: RenderFlags, ctx: App) {
298+
if (rf & RenderFlags.Create) {
299+
element(1, 'div', ['myDir']);
300+
}
301+
},
302+
viewQuery: function(rf: RenderFlags, ctx: App) {
303+
let tmp: any;
304+
if (rf & RenderFlags.Create) {
305+
query(0, MyDirective, false, Alias);
306+
}
307+
if (rf & RenderFlags.Update) {
308+
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.service = tmp.first);
309+
}
310+
},
311+
directives: [MyDirective]
312+
});
313+
}
314+
315+
const componentFixture = new ComponentFixture(App);
316+
expect(componentFixture.component.service).toBe(directive !.service);
317+
});
318+
319+
});
320+
208321
describe('local names', () => {
209322

210323
it('should query for a single element and read ElementRef by default', () => {
@@ -2122,7 +2235,7 @@ describe('query', () => {
21222235
this.contentCheckedQuerySnapshot = this.foos ? this.foos.length : 0;
21232236
}
21242237

2125-
static ngComponentDef = defineDirective({
2238+
static ngDirectiveDef = defineDirective({
21262239
type: WithContentDirective,
21272240
selectors: [['', 'with-content', '']],
21282241
factory: () => new WithContentDirective(),

0 commit comments

Comments
 (0)