feat(core): support injection token as predicate in queries (#37506)
Currently Angular internally already handles `InjectionToken` as predicates for queries. This commit exposes this as public API as developers already relied on this functionality but currently use workarounds to satisfy the type constraints (e.g. `as any`). We intend to make this public as it's low-effort to support, and it's a significant key part for the use of light-weight tokens as described in the upcoming guide: https://github.com/angular/angular/pull/36144. In concrete, applications might use injection tokens over classes for both optional DI and queries, because otherwise such references cause classes to be always retained. This was also an issue in View Engine, but now with Ivy, this pattern became worse, as factories are directly attached to retained classes (ultimately ending up in the production bundle, while being unused). More details in the light-weight token guide and in: https://github.com/angular/angular-cli/issues/16866. Closes #21152. Related to #36144. PR Close #37506
This commit is contained in:
parent
a937889c3b
commit
97dc85ba5e
|
@ -163,11 +163,11 @@ export declare interface ConstructorSansProvider {
|
|||
export declare type ContentChild = Query;
|
||||
|
||||
export declare interface ContentChildDecorator {
|
||||
(selector: Type<any> | Function | string, opts?: {
|
||||
(selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
read?: any;
|
||||
static?: boolean;
|
||||
}): any;
|
||||
new (selector: Type<any> | Function | string, opts?: {
|
||||
new (selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
read?: any;
|
||||
static?: boolean;
|
||||
}): ContentChild;
|
||||
|
@ -176,11 +176,11 @@ export declare interface ContentChildDecorator {
|
|||
export declare type ContentChildren = Query;
|
||||
|
||||
export declare interface ContentChildrenDecorator {
|
||||
(selector: Type<any> | Function | string, opts?: {
|
||||
(selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
descendants?: boolean;
|
||||
read?: any;
|
||||
}): any;
|
||||
new (selector: Type<any> | Function | string, opts?: {
|
||||
new (selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
descendants?: boolean;
|
||||
read?: any;
|
||||
}): Query;
|
||||
|
@ -725,7 +725,7 @@ export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportA
|
|||
|
||||
export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener;
|
||||
|
||||
export declare function ɵɵcontentQuery<T>(directiveIndex: number, predicate: Type<any> | string[], descend: boolean, read?: any): void;
|
||||
export declare function ɵɵcontentQuery<T>(directiveIndex: number, predicate: Type<any> | InjectionToken<unknown> | string[], descend: boolean, read?: any): void;
|
||||
|
||||
export declare function ɵɵCopyDefinitionFeature(definition: ɵDirectiveDef<any> | ɵComponentDef<any>): void;
|
||||
|
||||
|
@ -1008,9 +1008,9 @@ export declare function ɵɵsetNgModuleScope(type: any, scope: {
|
|||
exports?: Type<any>[] | (() => Type<any>[]);
|
||||
}): void;
|
||||
|
||||
export declare function ɵɵstaticContentQuery<T>(directiveIndex: number, predicate: Type<any> | string[], descend: boolean, read?: any): void;
|
||||
export declare function ɵɵstaticContentQuery<T>(directiveIndex: number, predicate: Type<any> | InjectionToken<unknown> | string[], descend: boolean, read?: any): void;
|
||||
|
||||
export declare function ɵɵstaticViewQuery<T>(predicate: Type<any> | string[], descend: boolean, read?: any): void;
|
||||
export declare function ɵɵstaticViewQuery<T>(predicate: Type<any> | InjectionToken<unknown> | string[], descend: boolean, read?: any): void;
|
||||
|
||||
export declare function ɵɵstyleMap(styles: {
|
||||
[styleName: string]: any;
|
||||
|
@ -1082,7 +1082,7 @@ export declare function ɵɵtextInterpolateV(values: any[]): typeof ɵɵtextInte
|
|||
|
||||
export declare function ɵɵupdateSyntheticHostBinding<T>(propName: string, value: T | ɵNO_CHANGE, sanitizer?: SanitizerFn | null): typeof ɵɵupdateSyntheticHostBinding;
|
||||
|
||||
export declare function ɵɵviewQuery<T>(predicate: Type<any> | string[], descend: boolean, read?: any): void;
|
||||
export declare function ɵɵviewQuery<T>(predicate: Type<any> | InjectionToken<unknown> | string[], descend: boolean, read?: any): void;
|
||||
|
||||
export declare const PACKAGE_ROOT_URL: InjectionToken<string>;
|
||||
|
||||
|
@ -1385,11 +1385,11 @@ export declare const VERSION: Version;
|
|||
export declare type ViewChild = Query;
|
||||
|
||||
export declare interface ViewChildDecorator {
|
||||
(selector: Type<any> | Function | string, opts?: {
|
||||
(selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
read?: any;
|
||||
static?: boolean;
|
||||
}): any;
|
||||
new (selector: Type<any> | Function | string, opts?: {
|
||||
new (selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
read?: any;
|
||||
static?: boolean;
|
||||
}): ViewChild;
|
||||
|
@ -1398,10 +1398,10 @@ export declare interface ViewChildDecorator {
|
|||
export declare type ViewChildren = Query;
|
||||
|
||||
export declare interface ViewChildrenDecorator {
|
||||
(selector: Type<any> | Function | string, opts?: {
|
||||
(selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
read?: any;
|
||||
}): any;
|
||||
new (selector: Type<any> | Function | string, opts?: {
|
||||
new (selector: Type<any> | InjectionToken<unknown> | Function | string, opts?: {
|
||||
read?: any;
|
||||
}): ViewChildren;
|
||||
}
|
||||
|
|
|
@ -2931,8 +2931,8 @@ runInEachFileSystem(os => {
|
|||
template: '<div></div>',
|
||||
})
|
||||
class FooCmp {
|
||||
@ViewChild(TOKEN as any) viewChild: any;
|
||||
@ContentChild(TOKEN as any) contentChild: any;
|
||||
@ViewChild(TOKEN) viewChild: any;
|
||||
@ContentChild(TOKEN) contentChild: any;
|
||||
}
|
||||
`);
|
||||
|
||||
|
|
|
@ -221,7 +221,8 @@ export interface R3QueryMetadata {
|
|||
first: boolean;
|
||||
|
||||
/**
|
||||
* Either an expression representing a type for the query predicate, or a set of string selectors.
|
||||
* Either an expression representing a type or `InjectionToken` for the query
|
||||
* predicate, or a set of string selectors.
|
||||
*/
|
||||
predicate: o.Expression|string[];
|
||||
|
||||
|
|
|
@ -157,8 +157,10 @@ export interface ContentChildrenDecorator {
|
|||
*
|
||||
* @Annotation
|
||||
*/
|
||||
(selector: Type<any>|Function|string, opts?: {descendants?: boolean, read?: any}): any;
|
||||
new(selector: Type<any>|Function|string, opts?: {descendants?: boolean, read?: any}): Query;
|
||||
(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {descendants?: boolean, read?: any}): any;
|
||||
new(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {descendants?: boolean, read?: any}): Query;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -218,8 +220,10 @@ export interface ContentChildDecorator {
|
|||
*
|
||||
* @Annotation
|
||||
*/
|
||||
(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): any;
|
||||
new(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): ContentChild;
|
||||
(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {read?: any, static?: boolean}): any;
|
||||
new(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {read?: any, static?: boolean}): ContentChild;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,8 +279,9 @@ export interface ViewChildrenDecorator {
|
|||
*
|
||||
* @Annotation
|
||||
*/
|
||||
(selector: Type<any>|Function|string, opts?: {read?: any}): any;
|
||||
new(selector: Type<any>|Function|string, opts?: {read?: any}): ViewChildren;
|
||||
(selector: Type<any>|InjectionToken<unknown>|Function|string, opts?: {read?: any}): any;
|
||||
new(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {read?: any}): ViewChildren;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -343,8 +348,10 @@ export interface ViewChildDecorator {
|
|||
*
|
||||
* @Annotation
|
||||
*/
|
||||
(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): any;
|
||||
new(selector: Type<any>|Function|string, opts?: {read?: any, static?: boolean}): ViewChild;
|
||||
(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {read?: any, static?: boolean}): any;
|
||||
new(selector: Type<any>|InjectionToken<unknown>|Function|string,
|
||||
opts?: {read?: any, static?: boolean}): ViewChild;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken} from '../../di/injection_token';
|
||||
import {Type} from '../../interface/type';
|
||||
import {QueryList} from '../../linker/query_list';
|
||||
|
||||
|
@ -16,7 +17,7 @@ import {TView} from './view';
|
|||
* An object representing query metadata extracted from query annotations.
|
||||
*/
|
||||
export interface TQueryMetadata {
|
||||
predicate: Type<any>|string[];
|
||||
predicate: Type<any>|InjectionToken<unknown>|string[];
|
||||
descendants: boolean;
|
||||
read: any;
|
||||
isStatic: boolean;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
// We are temporarily importing the existing viewEngine_from core so we can be sure we are
|
||||
// correctly implementing its interfaces for backwards compatibility.
|
||||
|
||||
import {InjectionToken} from '../di/injection_token';
|
||||
import {Type} from '../interface/type';
|
||||
import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref';
|
||||
import {QueryList} from '../linker/query_list';
|
||||
|
@ -89,8 +90,8 @@ class LQueries_ implements LQueries {
|
|||
|
||||
class TQueryMetadata_ implements TQueryMetadata {
|
||||
constructor(
|
||||
public predicate: Type<any>|string[], public descendants: boolean, public isStatic: boolean,
|
||||
public read: any = null) {}
|
||||
public predicate: Type<any>|InjectionToken<unknown>|string[], public descendants: boolean,
|
||||
public isStatic: boolean, public read: any = null) {}
|
||||
}
|
||||
|
||||
class TQueries_ implements TQueries {
|
||||
|
@ -454,7 +455,7 @@ export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
|
|||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵstaticViewQuery<T>(
|
||||
predicate: Type<any>|string[], descend: boolean, read?: any): void {
|
||||
predicate: Type<any>|InjectionToken<unknown>|string[], descend: boolean, read?: any): void {
|
||||
viewQueryInternal(getTView(), getLView(), predicate, descend, read, true);
|
||||
}
|
||||
|
||||
|
@ -467,13 +468,14 @@ export function ɵɵstaticViewQuery<T>(
|
|||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵviewQuery<T>(predicate: Type<any>|string[], descend: boolean, read?: any): void {
|
||||
export function ɵɵviewQuery<T>(
|
||||
predicate: Type<any>|InjectionToken<unknown>|string[], descend: boolean, read?: any): void {
|
||||
viewQueryInternal(getTView(), getLView(), predicate, descend, read, false);
|
||||
}
|
||||
|
||||
function viewQueryInternal<T>(
|
||||
tView: TView, lView: LView, predicate: Type<any>|string[], descend: boolean, read: any,
|
||||
isStatic: boolean): void {
|
||||
tView: TView, lView: LView, predicate: Type<any>|InjectionToken<unknown>|string[],
|
||||
descend: boolean, read: any, isStatic: boolean): void {
|
||||
if (tView.firstCreatePass) {
|
||||
createTQuery(tView, new TQueryMetadata_(predicate, descend, isStatic, read), -1);
|
||||
if (isStatic) {
|
||||
|
@ -496,7 +498,8 @@ function viewQueryInternal<T>(
|
|||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵcontentQuery<T>(
|
||||
directiveIndex: number, predicate: Type<any>|string[], descend: boolean, read?: any): void {
|
||||
directiveIndex: number, predicate: Type<any>|InjectionToken<unknown>|string[], descend: boolean,
|
||||
read?: any): void {
|
||||
contentQueryInternal(
|
||||
getTView(), getLView(), predicate, descend, read, false, getPreviousOrParentTNode(),
|
||||
directiveIndex);
|
||||
|
@ -515,15 +518,16 @@ export function ɵɵcontentQuery<T>(
|
|||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵstaticContentQuery<T>(
|
||||
directiveIndex: number, predicate: Type<any>|string[], descend: boolean, read?: any): void {
|
||||
directiveIndex: number, predicate: Type<any>|InjectionToken<unknown>|string[], descend: boolean,
|
||||
read?: any): void {
|
||||
contentQueryInternal(
|
||||
getTView(), getLView(), predicate, descend, read, true, getPreviousOrParentTNode(),
|
||||
directiveIndex);
|
||||
}
|
||||
|
||||
function contentQueryInternal<T>(
|
||||
tView: TView, lView: LView, predicate: Type<any>|string[], descend: boolean, read: any,
|
||||
isStatic: boolean, tNode: TNode, directiveIndex: number): void {
|
||||
tView: TView, lView: LView, predicate: Type<any>|InjectionToken<unknown>|string[],
|
||||
descend: boolean, read: any, isStatic: boolean, tNode: TNode, directiveIndex: number): void {
|
||||
if (tView.firstCreatePass) {
|
||||
createTQuery(tView, new TQueryMetadata_(predicate, descend, isStatic, read), tNode.index);
|
||||
saveContentQueryAndDirectiveIndex(tView, directiveIndex);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
|
||||
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, InjectionToken, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
@ -17,10 +17,23 @@ describe('query logic', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective,
|
||||
SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp,
|
||||
QueryCompWithChanges, StaticContentQueryDir, SuperDirectiveQueryTarget, SuperDirective,
|
||||
SubComponent
|
||||
AppComp,
|
||||
QueryComp,
|
||||
SimpleCompA,
|
||||
SimpleCompB,
|
||||
StaticViewQueryComp,
|
||||
TextDirective,
|
||||
SubclassStaticViewQueryComp,
|
||||
StaticContentQueryComp,
|
||||
SubclassStaticContentQueryComp,
|
||||
QueryCompWithChanges,
|
||||
StaticContentQueryDir,
|
||||
SuperDirectiveQueryTarget,
|
||||
SuperDirective,
|
||||
SubComponent,
|
||||
TestComponentWithToken,
|
||||
TestInjectionTokenContentQueries,
|
||||
TestInjectionTokenQueries,
|
||||
]
|
||||
});
|
||||
});
|
||||
|
@ -74,6 +87,19 @@ describe('query logic', () => {
|
|||
expect(comp.viewChildren.first).toBeAnInstanceOf(TemplateRef);
|
||||
});
|
||||
|
||||
it('should support selecting InjectionToken', () => {
|
||||
const fixture = TestBed.createComponent(TestInjectionTokenQueries);
|
||||
const instance = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(instance.viewFirstOption).toBeDefined();
|
||||
expect(instance.viewFirstOption instanceof TestComponentWithToken).toBe(true);
|
||||
expect(instance.viewOptions).toBeDefined();
|
||||
expect(instance.viewOptions.length).toBe(2);
|
||||
expect(instance.contentFirstOption).toBeUndefined();
|
||||
expect(instance.contentOptions).toBeDefined();
|
||||
expect(instance.contentOptions.length).toBe(0);
|
||||
});
|
||||
|
||||
onlyInIvy('multiple local refs are supported in Ivy')
|
||||
.it('should return TemplateRefs when templates are labeled and retrieved', () => {
|
||||
const template = `
|
||||
|
@ -360,6 +386,17 @@ describe('query logic', () => {
|
|||
expect(comp.contentChildren.first).toBeAnInstanceOf(SimpleCompA);
|
||||
});
|
||||
|
||||
it('should support selecting InjectionToken', () => {
|
||||
const fixture = TestBed.createComponent(TestInjectionTokenContentQueries);
|
||||
const instance =
|
||||
fixture.debugElement.query(By.directive(TestInjectionTokenQueries)).componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(instance.contentFirstOption).toBeDefined();
|
||||
expect(instance.contentFirstOption instanceof TestComponentWithToken).toBe(true);
|
||||
expect(instance.contentOptions).toBeDefined();
|
||||
expect(instance.contentOptions.length).toBe(2);
|
||||
});
|
||||
|
||||
onlyInIvy('multiple local refs are supported in Ivy')
|
||||
.it('should return Component instances when Components are labeled and retrieved', () => {
|
||||
const template = `
|
||||
|
@ -1771,3 +1808,39 @@ class SuperDirective {
|
|||
})
|
||||
class SubComponent extends SuperDirective {
|
||||
}
|
||||
|
||||
const MY_OPTION_TOKEN = new InjectionToken<TestComponentWithToken>('ComponentWithToken');
|
||||
|
||||
@Component({
|
||||
selector: 'my-option',
|
||||
template: 'Option',
|
||||
providers: [{provide: MY_OPTION_TOKEN, useExisting: TestComponentWithToken}],
|
||||
})
|
||||
class TestComponentWithToken {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-injection-token',
|
||||
template: `
|
||||
<my-option></my-option>
|
||||
<my-option></my-option>
|
||||
<ng-content></ng-content>
|
||||
`
|
||||
})
|
||||
class TestInjectionTokenQueries {
|
||||
@ViewChild(MY_OPTION_TOKEN) viewFirstOption!: TestComponentWithToken;
|
||||
@ViewChildren(MY_OPTION_TOKEN) viewOptions!: QueryList<TestComponentWithToken>;
|
||||
@ContentChild(MY_OPTION_TOKEN) contentFirstOption!: TestComponentWithToken;
|
||||
@ContentChildren(MY_OPTION_TOKEN) contentOptions!: QueryList<TestComponentWithToken>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<test-injection-token>
|
||||
<my-option></my-option>
|
||||
<my-option></my-option>
|
||||
</test-injection-token>
|
||||
`
|
||||
})
|
||||
class TestInjectionTokenContentQueries {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue