fix(core): pipes injecting viewProviders when used on a component host node (#36512)

The flag that determines whether something should be able to inject from `viewProviders` is opt-out and the pipes weren't opted out, resulting in them being able to see the viewProviders if they're placed on a component host node.

Fixes #36146.

PR Close #36512
This commit is contained in:
crisbeto 2020-04-17 06:30:04 +02:00 committed by Matias Niemelä
parent bb150c2704
commit 81d23b33ef
3 changed files with 137 additions and 1 deletions

View File

@ -71,7 +71,7 @@ import {stringifyForError} from './util/misc_utils';
*/ */
let includeViewProviders = true; let includeViewProviders = true;
function setIncludeViewProviders(v: boolean): boolean { export function setIncludeViewProviders(v: boolean): boolean {
const oldValue = includeViewProviders; const oldValue = includeViewProviders;
includeViewProviders = v; includeViewProviders = v;
return oldValue; return oldValue;

View File

@ -11,6 +11,7 @@ import {PipeTransform} from '../change_detection/pipe_transform';
import {setInjectImplementation} from '../di/injector_compatibility'; import {setInjectImplementation} from '../di/injector_compatibility';
import {getFactoryDef} from './definition'; import {getFactoryDef} from './definition';
import {setIncludeViewProviders} from './di';
import {store, ɵɵdirectiveInject} from './instructions/all'; import {store, ɵɵdirectiveInject} from './instructions/all';
import {PipeDef, PipeDefList} from './interfaces/definition'; import {PipeDef, PipeDefList} from './interfaces/definition';
import {HEADER_OFFSET, LView, TVIEW} from './interfaces/view'; import {HEADER_OFFSET, LView, TVIEW} from './interfaces/view';
@ -47,7 +48,12 @@ export function ɵɵpipe(index: number, pipeName: string): any {
const pipeFactory = pipeDef.factory || (pipeDef.factory = getFactoryDef(pipeDef.type, true)); const pipeFactory = pipeDef.factory || (pipeDef.factory = getFactoryDef(pipeDef.type, true));
const previousInjectImplementation = setInjectImplementation(ɵɵdirectiveInject); const previousInjectImplementation = setInjectImplementation(ɵɵdirectiveInject);
// DI for pipes is supposed to behave like directives when placed on a component
// host node, which means that we have to disable access to `viewProviders`.
const previousIncludeViewProviders = setIncludeViewProviders(false);
const pipeInstance = pipeFactory(); const pipeInstance = pipeFactory();
setIncludeViewProviders(previousIncludeViewProviders);
setInjectImplementation(previousInjectImplementation); setInjectImplementation(previousInjectImplementation);
store(tView, getLView(), index, pipeInstance); store(tView, getLView(), index, pipeInstance);
return pipeInstance; return pipeInstance;

View File

@ -2170,4 +2170,134 @@ describe('di', () => {
fixture.componentInstance.child.base, fixture.componentInstance.child.base,
'should not get dirA from parent, but create new dirB from the useFactory provider'); 'should not get dirA from parent, but create new dirB from the useFactory provider');
}); });
describe('provider access on the same node', () => {
const token = new InjectionToken<number>('token');
onlyInIvy('accessing providers on the same node through a pipe was not supported in ViewEngine')
.it('pipes should access providers from the component they are on', () => {
@Pipe({name: 'token'})
class TokenPipe {
constructor(@Inject(token) private _token: string) {}
transform(value: string): string {
return value + this._token;
}
}
@Component({
selector: 'child-comp',
template: '{{value}}',
providers: [{provide: token, useValue: 'child'}]
})
class ChildComp {
@Input() value: any;
}
@Component({
template: `<child-comp [value]="'' | token"></child-comp>`,
providers: [{provide: token, useValue: 'parent'}]
})
class App {
}
TestBed.configureTestingModule({declarations: [App, ChildComp, TokenPipe]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toBe('child');
});
it('pipes should not access viewProviders from the component they are on', () => {
@Pipe({name: 'token'})
class TokenPipe {
constructor(@Inject(token) private _token: string) {}
transform(value: string): string {
return value + this._token;
}
}
@Component({
selector: 'child-comp',
template: '{{value}}',
viewProviders: [{provide: token, useValue: 'child'}]
})
class ChildComp {
@Input() value: any;
}
@Component({
template: `<child-comp [value]="'' | token"></child-comp>`,
viewProviders: [{provide: token, useValue: 'parent'}]
})
class App {
}
TestBed.configureTestingModule({declarations: [App, ChildComp, TokenPipe]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toBe('parent');
});
it('directives should access providers from the component they are on', () => {
@Directive({selector: '[dir]'})
class Dir {
constructor(@Inject(token) public token: string) {}
}
@Component({
selector: 'child-comp',
template: '',
providers: [{provide: token, useValue: 'child'}],
})
class ChildComp {
}
@Component({
template: '<child-comp dir></child-comp>',
providers: [{provide: token, useValue: 'parent'}]
})
class App {
@ViewChild(Dir) dir!: Dir;
}
TestBed.configureTestingModule({declarations: [App, ChildComp, Dir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.dir.token).toBe('child');
});
it('directives should not access viewProviders from the component they are on', () => {
@Directive({selector: '[dir]'})
class Dir {
constructor(@Inject(token) public token: string) {}
}
@Component({
selector: 'child-comp',
template: '',
viewProviders: [{provide: token, useValue: 'child'}]
})
class ChildComp {
}
@Component({
template: '<child-comp dir></child-comp>',
viewProviders: [{provide: token, useValue: 'parent'}]
})
class App {
@ViewChild(Dir) dir!: Dir;
}
TestBed.configureTestingModule({declarations: [App, ChildComp, Dir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.dir.token).toBe('parent');
});
});
}); });