fix(core): queries not matching string injection tokens (#38321)

Queries weren't matching directives that provide themselves via string
injection tokens, because the assumption was that any string passed to
a query decorator refers to a template reference.

These changes make it so we match both template references and
providers while giving precedence to the template references.

Fixes #38313.
Fixes #38315.

PR Close #38321
This commit is contained in:
crisbeto 2020-08-01 11:57:29 +02:00 committed by Andrew Kushnir
parent 6da9e5851a
commit 5dc8d287aa
3 changed files with 187 additions and 10 deletions

View File

@ -495,8 +495,8 @@ function searchTokensOnInjector<T>(
* @returns Index of a found directive or provider, or null when none found.
*/
export function locateDirectiveOrProvider<T>(
tNode: TNode, tView: TView, token: Type<T>|InjectionToken<T>, canAccessViewProviders: boolean,
isHostSpecialCase: boolean|number): number|null {
tNode: TNode, tView: TView, token: Type<T>|InjectionToken<T>|string,
canAccessViewProviders: boolean, isHostSpecialCase: boolean|number): number|null {
const nodeProviderIndexes = tNode.providerIndexes;
const tInjectables = tView.data;
@ -510,7 +510,8 @@ export function locateDirectiveOrProvider<T>(
// When the host special case applies, only the viewProviders and the component are visible
const endIndex = isHostSpecialCase ? injectablesStart + cptViewProvidersCount : directiveEnd;
for (let i = startingIndex; i < endIndex; i++) {
const providerTokenOrDef = tInjectables[i] as InjectionToken<any>| Type<any>| DirectiveDef<any>;
const providerTokenOrDef =
tInjectables[i] as InjectionToken<any>| Type<any>| DirectiveDef<any>| string;
if (i < directivesStart && token === providerTokenOrDef ||
i >= directivesStart && (providerTokenOrDef as DirectiveDef<any>).type === token) {
return i;

View File

@ -227,20 +227,23 @@ class TQuery_ implements TQuery {
}
private matchTNode(tView: TView, tNode: TNode): void {
if (Array.isArray(this.metadata.predicate)) {
const localNames = this.metadata.predicate;
for (let i = 0; i < localNames.length; i++) {
this.matchTNodeWithReadOption(tView, tNode, getIdxOfMatchingSelector(tNode, localNames[i]));
const predicate = this.metadata.predicate;
if (Array.isArray(predicate)) {
for (let i = 0; i < predicate.length; i++) {
const name = predicate[i];
this.matchTNodeWithReadOption(tView, tNode, getIdxOfMatchingSelector(tNode, name));
// Also try matching the name to a provider since strings can be used as DI tokens too.
this.matchTNodeWithReadOption(
tView, tNode, locateDirectiveOrProvider(tNode, tView, name, false, false));
}
} else {
const typePredicate = this.metadata.predicate as any;
if (typePredicate === ViewEngine_TemplateRef) {
if ((predicate as any) === ViewEngine_TemplateRef) {
if (tNode.type === TNodeType.Container) {
this.matchTNodeWithReadOption(tView, tNode, -1);
}
} else {
this.matchTNodeWithReadOption(
tView, tNode, locateDirectiveOrProvider(tNode, tView, typePredicate, false, false));
tView, tNode, locateDirectiveOrProvider(tNode, tView, predicate, false, false));
}
}
}

View File

@ -1631,6 +1631,179 @@ describe('query logic', () => {
expect(groups[2]).toBeUndefined();
});
});
describe('querying for string token providers', () => {
@Directive({
selector: '[text-token]',
providers: [{provide: 'Token', useExisting: TextTokenDirective}],
})
class TextTokenDirective {
}
it('should match string injection token in a ViewChild query', () => {
@Component({template: '<div text-token></div>'})
class App {
@ViewChild('Token') token: any;
}
TestBed.configureTestingModule({declarations: [App, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.token).toBeAnInstanceOf(TextTokenDirective);
});
it('should give precedence to local reference if both a reference and a string injection token provider match a ViewChild query',
() => {
@Component({template: '<div text-token #Token></div>'})
class App {
@ViewChild('Token') token: any;
}
TestBed.configureTestingModule({declarations: [App, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.token).toBeAnInstanceOf(ElementRef);
});
it('should match string injection token in a ViewChildren query', () => {
@Component({template: '<div text-token></div>'})
class App {
@ViewChildren('Token') tokens!: QueryList<any>;
}
TestBed.configureTestingModule({declarations: [App, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const tokens = fixture.componentInstance.tokens;
expect(tokens.length).toBe(1);
expect(tokens.first).toBeAnInstanceOf(TextTokenDirective);
});
it('should match both string injection token and local reference inside a ViewChildren query',
() => {
@Component({template: '<div text-token #Token></div>'})
class App {
@ViewChildren('Token') tokens!: QueryList<any>;
}
TestBed.configureTestingModule({declarations: [App, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.tokens.toArray()).toEqual([
jasmine.any(ElementRef), jasmine.any(TextTokenDirective)
]);
});
it('should match string injection token in a ContentChild query', () => {
@Component({selector: 'has-query', template: '<ng-content></ng-content>'})
class HasQuery {
@ContentChild('Token') token: any;
}
@Component({template: '<has-query><div text-token></div></has-query>'})
class App {
@ViewChild(HasQuery) queryComp!: HasQuery;
}
TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.queryComp.token).toBeAnInstanceOf(TextTokenDirective);
});
it('should give precedence to local reference if both a reference and a string injection token provider match a ContentChild query',
() => {
@Component({selector: 'has-query', template: '<ng-content></ng-content>'})
class HasQuery {
@ContentChild('Token') token: any;
}
@Component({template: '<has-query><div text-token #Token></div></has-query>'})
class App {
@ViewChild(HasQuery) queryComp!: HasQuery;
}
TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.queryComp.token).toBeAnInstanceOf(ElementRef);
});
it('should match string injection token in a ContentChildren query', () => {
@Component({selector: 'has-query', template: '<ng-content></ng-content>'})
class HasQuery {
@ContentChildren('Token') tokens!: QueryList<any>;
}
@Component({template: '<has-query><div text-token></div></has-query>'})
class App {
@ViewChild(HasQuery) queryComp!: HasQuery;
}
TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const tokens = fixture.componentInstance.queryComp.tokens;
expect(tokens.length).toBe(1);
expect(tokens.first).toBeAnInstanceOf(TextTokenDirective);
});
it('should match both string injection token and local reference inside a ContentChildren query',
() => {
@Component({selector: 'has-query', template: '<ng-content></ng-content>'})
class HasQuery {
@ContentChildren('Token') tokens!: QueryList<any>;
}
@Component({template: '<has-query><div text-token #Token></div></has-query>'})
class App {
@ViewChild(HasQuery) queryComp!: HasQuery;
}
TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.queryComp.tokens.toArray()).toEqual([
jasmine.any(ElementRef), jasmine.any(TextTokenDirective)
]);
});
it('should match string token specified through the `read` option of a view query', () => {
@Component({template: '<div text-token #Token></div>'})
class App {
@ViewChild('Token', {read: 'Token'}) token: any;
}
TestBed.configureTestingModule({declarations: [App, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.token).toBeAnInstanceOf(TextTokenDirective);
});
it('should match string token specified through the `read` option of a content query', () => {
@Component({selector: 'has-query', template: '<ng-content></ng-content>'})
class HasQuery {
@ContentChild('Token', {read: 'Token'}) token: any;
}
@Component({template: '<has-query><div text-token #Token></div></has-query>'})
class App {
@ViewChild(HasQuery) queryComp!: HasQuery;
}
TestBed.configureTestingModule({declarations: [App, HasQuery, TextTokenDirective]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.queryComp.token).toBeAnInstanceOf(TextTokenDirective);
});
});
});
function initWithTemplate(compType: Type<any>, template: string) {