Skip to content

Commit 49fca35

Browse files
committed
feat(core): support injection tokens as predicate in queries
Currently Angular internally already handles `InjectionToken` as predicates for queries. This commit exposes this as public API as people already relied on this functionality but currently used workarounds to satisfy the type constraints (e.g. `as any`). We intend to make this public as it low-effort, and it's a signficant key part for the use of light-weight tokens as described in the upcoming guide: angular#36144. In concrete, applications might use injection tokens over class for both DI and queries, because otherwise such class references for optional queries or DI always cause these classes to be retained. This was also an issue in View Engine, but now with Ivy, this pattern became worse as factories could be directly attached to retained classes (ultimately ending up in the production bundle, while unused). More details in the light-weight token guide and in: https://github.com/angular/angular-cli/issues/16866. Closes angular#21152.
1 parent 0cb0f66 commit 49fca35

File tree

7 files changed

+124
-39
lines changed

7 files changed

+124
-39
lines changed

goldens/public-api/core/core.d.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ export declare interface ConstructorSansProvider {
163163
export declare type ContentChild = Query;
164164

165165
export declare interface ContentChildDecorator {
166-
(selector: Type<any> | Function | string, opts?: {
166+
(selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
167167
read?: any;
168168
static?: boolean;
169169
}): any;
170-
new (selector: Type<any> | Function | string, opts?: {
170+
new (selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
171171
read?: any;
172172
static?: boolean;
173173
}): ContentChild;
@@ -176,11 +176,11 @@ export declare interface ContentChildDecorator {
176176
export declare type ContentChildren = Query;
177177

178178
export declare interface ContentChildrenDecorator {
179-
(selector: Type<any> | Function | string, opts?: {
179+
(selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
180180
descendants?: boolean;
181181
read?: any;
182182
}): any;
183-
new (selector: Type<any> | Function | string, opts?: {
183+
new (selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
184184
descendants?: boolean;
185185
read?: any;
186186
}): Query;
@@ -725,7 +725,7 @@ export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportA
725725

726726
export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener;
727727

728-
export declare function ɵɵcontentQuery<T>(directiveIndex: number, predicate: Type<any> | string[], descend: boolean, read?: any): void;
728+
export declare function ɵɵcontentQuery<T>(directiveIndex: number, predicate: Type<any> | InjectionToken<any> | string[], descend: boolean, read?: any): void;
729729

730730
export declare function ɵɵCopyDefinitionFeature(definition: ɵDirectiveDef<any> | ɵComponentDef<any>): void;
731731

@@ -1008,9 +1008,9 @@ export declare function ɵɵsetNgModuleScope(type: any, scope: {
10081008
exports?: Type<any>[] | (() => Type<any>[]);
10091009
}): void;
10101010

1011-
export declare function ɵɵstaticContentQuery<T>(directiveIndex: number, predicate: Type<any> | string[], descend: boolean, read?: any): void;
1011+
export declare function ɵɵstaticContentQuery<T>(directiveIndex: number, predicate: Type<any> | InjectionToken<any> | string[], descend: boolean, read?: any): void;
10121012

1013-
export declare function ɵɵstaticViewQuery<T>(predicate: Type<any> | string[], descend: boolean, read?: any): void;
1013+
export declare function ɵɵstaticViewQuery<T>(predicate: Type<any> | InjectionToken<any> | string[], descend: boolean, read?: any): void;
10141014

10151015
export declare function ɵɵstyleMap(styles: {
10161016
[styleName: string]: any;
@@ -1082,7 +1082,7 @@ export declare function ɵɵtextInterpolateV(values: any[]): typeof ɵɵtextInte
10821082

10831083
export declare function ɵɵupdateSyntheticHostBinding<T>(propName: string, value: T | ɵNO_CHANGE, sanitizer?: SanitizerFn | null): typeof ɵɵupdateSyntheticHostBinding;
10841084

1085-
export declare function ɵɵviewQuery<T>(predicate: Type<any> | string[], descend: boolean, read?: any): void;
1085+
export declare function ɵɵviewQuery<T>(predicate: Type<any> | InjectionToken<any> | string[], descend: boolean, read?: any): void;
10861086

10871087
export declare const PACKAGE_ROOT_URL: InjectionToken<string>;
10881088

@@ -1385,11 +1385,11 @@ export declare const VERSION: Version;
13851385
export declare type ViewChild = Query;
13861386

13871387
export declare interface ViewChildDecorator {
1388-
(selector: Type<any> | Function | string, opts?: {
1388+
(selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
13891389
read?: any;
13901390
static?: boolean;
13911391
}): any;
1392-
new (selector: Type<any> | Function | string, opts?: {
1392+
new (selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
13931393
read?: any;
13941394
static?: boolean;
13951395
}): ViewChild;
@@ -1398,10 +1398,10 @@ export declare interface ViewChildDecorator {
13981398
export declare type ViewChildren = Query;
13991399

14001400
export declare interface ViewChildrenDecorator {
1401-
(selector: Type<any> | Function | string, opts?: {
1401+
(selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
14021402
read?: any;
14031403
}): any;
1404-
new (selector: Type<any> | Function | string, opts?: {
1404+
new (selector: Type<any> | InjectionToken<any> | Function | string, opts?: {
14051405
read?: any;
14061406
}): ViewChildren;
14071407
}

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2931,8 +2931,8 @@ runInEachFileSystem(os => {
29312931
template: '<div></div>',
29322932
})
29332933
class FooCmp {
2934-
@ViewChild(TOKEN as any) viewChild: any;
2935-
@ContentChild(TOKEN as any) contentChild: any;
2934+
@ViewChild(TOKEN) viewChild: any;
2935+
@ContentChild(TOKEN) contentChild: any;
29362936
}
29372937
`);
29382938

packages/compiler/src/render3/view/api.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ export interface R3QueryMetadata {
221221
first: boolean;
222222

223223
/**
224-
* Either an expression representing a type for the query predicate, or a set of string selectors.
224+
* Either an expression representing a type or `InjectionToken` for the query
225+
* predicate, or a set of string selectors.
225226
*/
226227
predicate: o.Expression|string[];
227228

packages/core/src/metadata/di.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,10 @@ export interface ContentChildrenDecorator {
157157
*
158158
* @Annotation
159159
*/
160-
(selector: Type<any>|Function|string, opts?: {descendants?: boolean, read?: any}): any;
161-
new(selector: Type<any>|Function|string, opts?: {descendants?: boolean, read?: any}): Query;
160+
(selector: Type<any>|InjectionToken<any>|Function|string,
161+
opts?: {descendants?: boolean, read?: any}): any;
162+
new(selector: Type<any>|InjectionToken<any>|Function|string,
163+
opts?: {descendants?: boolean, read?: any}): Query;
162164
}
163165

164166
/**
@@ -218,8 +220,10 @@ export interface ContentChildDecorator {
218220
*
219221
* @Annotation
220222
*/
221-
(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): any;
222-
new(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): ContentChild;
223+
(selector: Type<any>|InjectionToken<any>|Function|string,
224+
opts?: {read?: any, static?: boolean}): any;
225+
new(selector: Type<any>|InjectionToken<any>|Function|string,
226+
opts?: {read?: any, static?: boolean}): ContentChild;
223227
}
224228

225229
/**
@@ -275,8 +279,8 @@ export interface ViewChildrenDecorator {
275279
*
276280
* @Annotation
277281
*/
278-
(selector: Type<any>|Function|string, opts?: {read?: any}): any;
279-
new(selector: Type<any>|Function|string, opts?: {read?: any}): ViewChildren;
282+
(selector: Type<any>|InjectionToken<any>|Function|string, opts?: {read?: any}): any;
283+
new(selector: Type<any>|InjectionToken<any>|Function|string, opts?: {read?: any}): ViewChildren;
280284
}
281285

282286
/**
@@ -343,8 +347,10 @@ export interface ViewChildDecorator {
343347
*
344348
* @Annotation
345349
*/
346-
(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): any;
347-
new(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): ViewChild;
350+
(selector: Type<any>|InjectionToken<any>|Function|string,
351+
opts?: {read?: any, static?: boolean}): any;
352+
new(selector: Type<any>|InjectionToken<any>|Function|string,
353+
opts?: {read?: any, static?: boolean}): ViewChild;
348354
}
349355

350356
/**

packages/core/src/render3/interfaces/query.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {InjectionToken} from '../../di/injection_token';
910
import {Type} from '../../interface/type';
1011
import {QueryList} from '../../linker/query_list';
1112

@@ -16,7 +17,7 @@ import {TView} from './view';
1617
* An object representing query metadata extracted from query annotations.
1718
*/
1819
export interface TQueryMetadata {
19-
predicate: Type<any>|string[];
20+
predicate: Type<any>|InjectionToken<any>|string[];
2021
descendants: boolean;
2122
read: any;
2223
isStatic: boolean;

packages/core/src/render3/query.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// We are temporarily importing the existing viewEngine_from core so we can be sure we are
1010
// correctly implementing its interfaces for backwards compatibility.
1111

12+
import {InjectionToken} from '../di/injection_token';
1213
import {Type} from '../interface/type';
1314
import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref';
1415
import {QueryList} from '../linker/query_list';
@@ -89,8 +90,8 @@ class LQueries_ implements LQueries {
8990

9091
class TQueryMetadata_ implements TQueryMetadata {
9192
constructor(
92-
public predicate: Type<any>|string[], public descendants: boolean, public isStatic: boolean,
93-
public read: any = null) {}
93+
public predicate: Type<any>|InjectionToken<any>|string[], public descendants: boolean,
94+
public isStatic: boolean, public read: any = null) {}
9495
}
9596

9697
class TQueries_ implements TQueries {
@@ -454,7 +455,7 @@ export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
454455
* @codeGenApi
455456
*/
456457
export function ɵɵstaticViewQuery<T>(
457-
predicate: Type<any>|string[], descend: boolean, read?: any): void {
458+
predicate: Type<any>|InjectionToken<any>|string[], descend: boolean, read?: any): void {
458459
viewQueryInternal(getTView(), getLView(), predicate, descend, read, true);
459460
}
460461

@@ -467,13 +468,14 @@ export function ɵɵstaticViewQuery<T>(
467468
*
468469
* @codeGenApi
469470
*/
470-
export function ɵɵviewQuery<T>(predicate: Type<any>|string[], descend: boolean, read?: any): void {
471+
export function ɵɵviewQuery<T>(
472+
predicate: Type<any>|InjectionToken<any>|string[], descend: boolean, read?: any): void {
471473
viewQueryInternal(getTView(), getLView(), predicate, descend, read, false);
472474
}
473475

474476
function viewQueryInternal<T>(
475-
tView: TView, lView: LView, predicate: Type<any>|string[], descend: boolean, read: any,
476-
isStatic: boolean): void {
477+
tView: TView, lView: LView, predicate: Type<any>|InjectionToken<any>|string[], descend: boolean,
478+
read: any, isStatic: boolean): void {
477479
if (tView.firstCreatePass) {
478480
createTQuery(tView, new TQueryMetadata_(predicate, descend, isStatic, read), -1);
479481
if (isStatic) {
@@ -496,7 +498,8 @@ function viewQueryInternal<T>(
496498
* @codeGenApi
497499
*/
498500
export function ɵɵcontentQuery<T>(
499-
directiveIndex: number, predicate: Type<any>|string[], descend: boolean, read?: any): void {
501+
directiveIndex: number, predicate: Type<any>|InjectionToken<any>|string[], descend: boolean,
502+
read?: any): void {
500503
contentQueryInternal(
501504
getTView(), getLView(), predicate, descend, read, false, getPreviousOrParentTNode(),
502505
directiveIndex);
@@ -515,15 +518,16 @@ export function ɵɵcontentQuery<T>(
515518
* @codeGenApi
516519
*/
517520
export function ɵɵstaticContentQuery<T>(
518-
directiveIndex: number, predicate: Type<any>|string[], descend: boolean, read?: any): void {
521+
directiveIndex: number, predicate: Type<any>|InjectionToken<any>|string[], descend: boolean,
522+
read?: any): void {
519523
contentQueryInternal(
520524
getTView(), getLView(), predicate, descend, read, true, getPreviousOrParentTNode(),
521525
directiveIndex);
522526
}
523527

524528
function contentQueryInternal<T>(
525-
tView: TView, lView: LView, predicate: Type<any>|string[], descend: boolean, read: any,
526-
isStatic: boolean, tNode: TNode, directiveIndex: number): void {
529+
tView: TView, lView: LView, predicate: Type<any>|InjectionToken<any>|string[], descend: boolean,
530+
read: any, isStatic: boolean, tNode: TNode, directiveIndex: number): void {
527531
if (tView.firstCreatePass) {
528532
createTQuery(tView, new TQueryMetadata_(predicate, descend, isStatic, read), tNode.index);
529533
saveContentQueryAndDirectiveIndex(tView, directiveIndex);

packages/core/test/acceptance/query_spec.ts

+78-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10-
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
10+
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, InjectionToken, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
1111
import {TestBed} from '@angular/core/testing';
1212
import {By} from '@angular/platform-browser';
1313
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -17,10 +17,23 @@ describe('query logic', () => {
1717
beforeEach(() => {
1818
TestBed.configureTestingModule({
1919
declarations: [
20-
AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective,
21-
SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp,
22-
QueryCompWithChanges, StaticContentQueryDir, SuperDirectiveQueryTarget, SuperDirective,
23-
SubComponent
20+
AppComp,
21+
QueryComp,
22+
SimpleCompA,
23+
SimpleCompB,
24+
StaticViewQueryComp,
25+
TextDirective,
26+
SubclassStaticViewQueryComp,
27+
StaticContentQueryComp,
28+
SubclassStaticContentQueryComp,
29+
QueryCompWithChanges,
30+
StaticContentQueryDir,
31+
SuperDirectiveQueryTarget,
32+
SuperDirective,
33+
SubComponent,
34+
TestComponentWithToken,
35+
TestInjectionTokenContentQueries,
36+
TestInjectionTokenQueries,
2437
]
2538
});
2639
});
@@ -74,6 +87,19 @@ describe('query logic', () => {
7487
expect(comp.viewChildren.first).toBeAnInstanceOf(TemplateRef);
7588
});
7689

90+
it('should support selecting InjectionToken', () => {
91+
const fixture = TestBed.createComponent(TestInjectionTokenQueries);
92+
const instance = fixture.componentInstance;
93+
fixture.detectChanges();
94+
expect(instance.viewFirstOption).toBeDefined();
95+
expect(instance.viewFirstOption instanceof TestComponentWithToken).toBe(true);
96+
expect(instance.viewOptions).toBeDefined();
97+
expect(instance.viewOptions.length).toBe(2);
98+
expect(instance.contentFirstOption).toBeUndefined();
99+
expect(instance.contentOptions).toBeDefined();
100+
expect(instance.contentOptions.length).toBe(0);
101+
});
102+
77103
onlyInIvy('multiple local refs are supported in Ivy')
78104
.it('should return TemplateRefs when templates are labeled and retrieved', () => {
79105
const template = `
@@ -360,6 +386,17 @@ describe('query logic', () => {
360386
expect(comp.contentChildren.first).toBeAnInstanceOf(SimpleCompA);
361387
});
362388

389+
it('should support selecting InjectionToken', () => {
390+
const fixture = TestBed.createComponent(TestInjectionTokenContentQueries);
391+
const instance =
392+
fixture.debugElement.query(By.directive(TestInjectionTokenQueries)).componentInstance;
393+
fixture.detectChanges();
394+
expect(instance.contentFirstOption).toBeDefined();
395+
expect(instance.contentFirstOption instanceof TestComponentWithToken).toBe(true);
396+
expect(instance.contentOptions).toBeDefined();
397+
expect(instance.contentOptions.length).toBe(2);
398+
});
399+
363400
onlyInIvy('multiple local refs are supported in Ivy')
364401
.it('should return Component instances when Components are labeled and retrieved', () => {
365402
const template = `
@@ -1771,3 +1808,39 @@ class SuperDirective {
17711808
})
17721809
class SubComponent extends SuperDirective {
17731810
}
1811+
1812+
const MY_OPTION_TOKEN = new InjectionToken<TestComponentWithToken>('ComponentWithToken');
1813+
1814+
@Component({
1815+
selector: 'my-option',
1816+
template: 'Option',
1817+
providers: [{provide: MY_OPTION_TOKEN, useExisting: TestComponentWithToken}],
1818+
})
1819+
class TestComponentWithToken {
1820+
}
1821+
1822+
@Component({
1823+
selector: 'test-injection-token',
1824+
template: `
1825+
<my-option></my-option>
1826+
<my-option></my-option>
1827+
<ng-content></ng-content>
1828+
`
1829+
})
1830+
class TestInjectionTokenQueries {
1831+
@ViewChild(MY_OPTION_TOKEN) viewFirstOption!: TestComponentWithToken;
1832+
@ViewChildren(MY_OPTION_TOKEN) viewOptions!: QueryList<TestComponentWithToken>;
1833+
@ContentChild(MY_OPTION_TOKEN) contentFirstOption!: TestComponentWithToken;
1834+
@ContentChildren(MY_OPTION_TOKEN) contentOptions!: QueryList<TestComponentWithToken>;
1835+
}
1836+
1837+
@Component({
1838+
template: `
1839+
<test-injection-token>
1840+
<my-option></my-option>
1841+
<my-option></my-option>
1842+
</test-injection-token>
1843+
`
1844+
})
1845+
class TestInjectionTokenContentQueries {
1846+
}

0 commit comments

Comments
 (0)