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:
parent
6da9e5851a
commit
5dc8d287aa
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue