Skip to content

Commit

Permalink
fix(core): take @host into account while processing useFactory argu…
Browse files Browse the repository at this point in the history
…ments (#40122) (#40313)

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

PR Close #40313
  • Loading branch information
AndrewKushnir authored and josephperrott committed Jan 5, 2021
1 parent ec5b09b commit 45838c0
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 1 deletion.
5 changes: 4 additions & 1 deletion packages/core/src/di/injector_compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import '../util/ng_dev_mode';
import {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 = {};
Expand Down Expand Up @@ -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 {
Expand Down
100 changes: 100 additions & 0 deletions packages/core/test/acceptance/di_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/forms/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@
{
"name": "FormsModule"
},
{
"name": "Host"
},
{
"name": "INJECTOR"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
{
"name": "EMPTY_ARRAY"
},
{
"name": "Host"
},
{
"name": "INJECTOR"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@
{
"name": "HashLocationStrategy"
},
{
"name": "Host"
},
{
"name": "INITIAL_VALUE"
},
Expand Down

0 comments on commit 45838c0

Please sign in to comment.