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:
parent
212245f197
commit
3735633bb0
|
@ -3,7 +3,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 1485,
|
||||
"main-es2015": 140333,
|
||||
"main-es2015": 140871,
|
||||
"polyfills-es2015": 36964
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -236,6 +236,9 @@
|
|||
{
|
||||
"name": "FormsModule"
|
||||
},
|
||||
{
|
||||
"name": "Host"
|
||||
},
|
||||
{
|
||||
"name": "INJECTOR"
|
||||
},
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
{
|
||||
"name": "EMPTY_ARRAY"
|
||||
},
|
||||
{
|
||||
"name": "Host"
|
||||
},
|
||||
{
|
||||
"name": "INJECTOR"
|
||||
},
|
||||
|
|
|
@ -290,6 +290,9 @@
|
|||
{
|
||||
"name": "HashLocationStrategy"
|
||||
},
|
||||
{
|
||||
"name": "Host"
|
||||
},
|
||||
{
|
||||
"name": "INITIAL_VALUE"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue