fix(core): take @Host into account while processing `useFactory` arguments (#40122)

DI providers can be defined via `useFactory` function, which may have arguments configured via `deps` array.
The `deps` array may contain DI flags represented by DI decorators (such as `@Self`, `@SkipSelf`, etc). Prior to this
commit, having the `@Host` decorator in `deps` array resulted in runtime error in Ivy. The problem was that the `@Host`
decorator was not taken into account while `useFactory` argument list was constructed, the `@Host` decorator was
treated as a token that should be looked up.

This commit updates the logic which prepares `useFactory` arguments to recognize the `@Host` decorator.

PR Close #40122
This commit is contained in:
Andrew Kushnir 2020-12-14 23:07:29 -08:00 committed by Joey Perrott
parent 212245f197
commit 3735633bb0
6 changed files with 114 additions and 2 deletions

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 140333,
"main-es2015": 140871,
"polyfills-es2015": 36964
}
}

View File

@ -11,13 +11,14 @@ import '../util/ng_dev_mode';
import {AbstractType, Type} from '../interface/type';
import {getClosureSafeProperty} from '../util/property';
import {stringify} from '../util/stringify';
import {resolveForwardRef} from './forward_ref';
import {getInjectImplementation, injectRootLimpMode} from './inject_switch';
import {InjectionToken} from './injection_token';
import {Injector} from './injector';
import {InjectFlags} from './interface/injector';
import {ValueProvider} from './interface/provider';
import {Inject, Optional, Self, SkipSelf} from './metadata';
import {Host, Inject, Optional, Self, SkipSelf} from './metadata';
const _THROW_IF_NOT_FOUND = {};
@ -151,6 +152,8 @@ export function injectArgs(types: (Type<any>|InjectionToken<any>|any[])[]): any[
flags |= InjectFlags.SkipSelf;
} else if (meta instanceof Self || meta.ngMetadataName === 'Self' || meta === Self) {
flags |= InjectFlags.Self;
} else if (meta instanceof Host || meta.ngMetadataName === 'Host' || meta === Host) {
flags |= InjectFlags.Host;
} else if (meta instanceof Inject || meta === Inject) {
type = meta.token;
} else {

View File

@ -2803,6 +2803,106 @@ describe('di', () => {
});
});
it('should be able to use Host in `useFactory` dependency config', () => {
// Scenario:
// ---------
// <root (provides token A)>
// <comp (provides token B via useFactory(@Host() @Inject(A))></comp>
// </root>
@Component({
selector: 'root',
template: '<comp></comp>',
viewProviders: [{
provide: 'A',
useValue: 'A from Root',
}]
})
class Root {
}
@Component({
selector: 'comp',
template: '{{ token }}',
viewProviders: [{
provide: 'B',
deps: [[new Inject('A'), new Host()]],
useFactory: (token: string) => `${token} (processed by useFactory)`,
}]
})
class Comp {
constructor(@Inject('B') readonly token: string) {}
}
@Component({
template: `<root></root>`,
})
class App {
}
TestBed.configureTestingModule({declarations: [Root, Comp, App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('A from Root (processed by useFactory)');
});
it('should not lookup outside of the host element when Host is used in `useFactory`', () => {
// Scenario:
// ---------
// <root (provides token A)>
// <intermediate>
// <comp (provides token B via useFactory(@Host() @Inject(A))></comp>
// </intermediate>
// </root>
@Component({
selector: 'root',
template: '<intermediate></intermediate>',
viewProviders: [{
provide: 'A',
useValue: 'A from Root',
}]
})
class Root {
}
@Component({
selector: 'intermediate',
template: '<comp></comp>',
})
class Intermediate {
}
@Component({
selector: 'comp',
template: '{{ token }}',
viewProviders: [{
provide: 'B',
deps: [[new Inject('A'), new Host(), new Optional()]],
useFactory: (token: string) =>
token ? `${token} (processed by useFactory)` : 'No token A found',
}]
})
class Comp {
constructor(@Inject('B') readonly token: string) {}
}
@Component({
template: `<root></root>`,
})
class App {
}
TestBed.configureTestingModule({declarations: [Root, Comp, App, Intermediate]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
// Making sure that the `@Host` takes effect and token `A` becomes unavailable in DI since it's
// defined one level up from the Comp's host view.
expect(fixture.nativeElement.textContent).toBe('No token A found');
});
it('should not cause cyclic dependency if same token is requested in deps with @SkipSelf', () => {
@Component({
selector: 'my-comp',

View File

@ -236,6 +236,9 @@
{
"name": "FormsModule"
},
{
"name": "Host"
},
{
"name": "INJECTOR"
},

View File

@ -5,6 +5,9 @@
{
"name": "EMPTY_ARRAY"
},
{
"name": "Host"
},
{
"name": "INJECTOR"
},

View File

@ -290,6 +290,9 @@
{
"name": "HashLocationStrategy"
},
{
"name": "Host"
},
{
"name": "INITIAL_VALUE"
},