/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule} from '@angular/common';
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ContentChild, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs';
describe('di', () => {
describe('no dependencies', () => {
it('should create directive with no deps', () => {
@Directive({selector: '[dir]', exportAs: 'dir'})
class MyDirective {
value = 'Created';
}
@Component({template: '
`
})
class MyApp {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const divElement = fixture.nativeElement.querySelector('div');
expect(divElement.textContent).toEqual('DirBDirB');
});
it('should create injectors and host bindings in same view', () => {
@Directive({selector: '[hostBindingDir]'})
class HostBindingDirective {
@HostBinding('id') id = 'foo';
}
@Component({
template: `
{{ dir.dirB.value }}
`
})
class MyApp {
@ViewChild(HostBindingDirective) hostBindingDir!: HostBindingDirective;
@ViewChild(DirectiveA) dirA!: DirectiveA;
}
TestBed.configureTestingModule(
{declarations: [DirectiveA, DirectiveB, HostBindingDirective, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const divElement = fixture.nativeElement.querySelector('div');
expect(divElement.textContent).toEqual('DirB');
expect(divElement.id).toEqual('foo');
const dirA = fixture.componentInstance.dirA;
expect(dirA.vcr.injector).toEqual(dirA.injector);
const hostBindingDir = fixture.componentInstance.hostBindingDir;
hostBindingDir.id = 'bar';
fixture.detectChanges();
expect(divElement.id).toBe('bar');
});
it('dynamic components should find dependencies when parent is projected', () => {
@Directive({selector: '[dirA]'})
class DirA {
}
@Directive({selector: '[dirB]'})
class DirB {
}
@Component({selector: 'child', template: ''})
class Child {
constructor(@Optional() readonly dirA: DirA, @Optional() readonly dirB: DirB) {}
}
@Component({
selector: 'projector',
template: '',
})
class Projector {
}
@Component({
template: `
`,
entryComponents: [Child]
})
class MyApp {
@ViewChild('childOrigin', {read: ViewContainerRef, static: true})
childOrigin!: ViewContainerRef;
@ViewChild('childOriginWithDirB', {read: ViewContainerRef, static: true})
childOriginWithDirB!: ViewContainerRef;
childFactory = this.resolver.resolveComponentFactory(Child);
constructor(readonly resolver: ComponentFactoryResolver, readonly injector: Injector) {}
addChild() {
return this.childOrigin.createComponent(this.childFactory);
}
addChildWithDirB() {
return this.childOriginWithDirB.createComponent(this.childFactory);
}
}
const fixture =
TestBed.configureTestingModule({declarations: [Child, DirA, DirB, Projector, MyApp]})
.createComponent(MyApp);
const child = fixture.componentInstance.addChild();
expect(child).toBeDefined();
expect(child.instance.dirA)
.not.toBeNull('dirA should be found. It is on the parent of the viewContainerRef.');
const child2 = fixture.componentInstance.addChildWithDirB();
expect(child2).toBeDefined();
expect(child2.instance.dirA)
.not.toBeNull('dirA should be found. It is on the parent of the viewContainerRef.');
expect(child2.instance.dirB)
.toBeNull(
'dirB appears on the ng-container and should not be found because the ' +
'viewContainerRef.createComponent node is inserted next to the container.');
});
});
it('should throw if directive is not found anywhere', () => {
@Directive({selector: '[dirB]'})
class DirectiveB {
constructor() {}
}
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(siblingDir: DirectiveB) {}
}
@Component({template: ''})
class MyComp {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/);
});
it('should throw if directive is not found in ancestor tree', () => {
@Directive({selector: '[dirB]'})
class DirectiveB {
constructor() {}
}
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(siblingDir: DirectiveB) {}
}
@Component({template: ''})
class MyComp {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/);
});
onlyInIvy('Ivy has different error message for circular dependency')
.it('should throw if directives try to inject each other', () => {
@Directive({selector: '[dirB]'})
class DirectiveB {
constructor(@Inject(forwardRef(() => DirectiveA)) siblingDir: DirectiveA) {}
}
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(siblingDir: DirectiveB) {}
}
@Component({template: ''})
class MyComp {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError(
'NG0200: Circular dependency in DI detected for DirectiveA. Find more at https://angular.io/errors/NG0200');
});
onlyInIvy('Ivy has different error message for circular dependency')
.it('should throw if directive tries to inject itself', () => {
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(siblingDir: DirectiveA) {}
}
@Component({template: ''})
class MyComp {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError(
'NG0200: Circular dependency in DI detected for DirectiveA. Find more at https://angular.io/errors/NG0200');
});
describe('flags', () => {
@Directive({selector: '[dirB]'})
class DirectiveB {
@Input('dirB') value = '';
}
describe('Optional', () => {
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(@Optional() public dirB: DirectiveB) {}
}
it('should not throw if dependency is @Optional (module injector)', () => {
@Component({template: ''})
class MyComp {
@ViewChild(DirectiveA) dirA!: DirectiveA;
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const dirA = fixture.componentInstance.dirA;
expect(dirA.dirB).toBeNull();
});
it('should return null if @Optional dependency has @Self flag', () => {
@Directive({selector: '[dirC]'})
class DirectiveC {
constructor(@Optional() @Self() public dirB: DirectiveB) {}
}
@Component({template: ''})
class MyComp {
@ViewChild(DirectiveC) dirC!: DirectiveC;
}
TestBed.configureTestingModule({declarations: [DirectiveC, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const dirC = fixture.componentInstance.dirC;
expect(dirC.dirB).toBeNull();
});
it('should not throw if dependency is @Optional but defined elsewhere', () => {
@Directive({selector: '[dirC]'})
class DirectiveC {
constructor(@Optional() public dirB: DirectiveB) {}
}
@Component({template: ''})
class MyComp {
@ViewChild(DirectiveC) dirC!: DirectiveC;
}
TestBed.configureTestingModule({declarations: [DirectiveB, DirectiveC, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const dirC = fixture.componentInstance.dirC;
expect(dirC.dirB).toBeNull();
});
});
onlyInIvy('Ivy has different error message when dependency is not found')
.it('should check only the current node with @Self', () => {
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(@Self() public dirB: DirectiveB) {}
}
@Component({template: '
'})
class MyComp {
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/);
});
describe('SkipSelf', () => {
describe('Injectors', () => {
it('should support @SkipSelf when injecting Injectors', () => {
@Component({
selector: 'parent',
template: '',
providers: [{
provide: 'token',
useValue: 'PARENT',
}]
})
class ParentComponent {
}
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class ChildComponent {
constructor(public injector: Injector, @SkipSelf() public parentInjector: Injector) {}
}
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
const childComponent =
fixture.debugElement.query(By.directive(ChildComponent)).componentInstance;
expect(childComponent.injector.get('token')).toBe('CHILD');
expect(childComponent.parentInjector.get('token')).toBe('PARENT');
});
it('should lookup module injector in case @SkipSelf is used and no suitable Injector found in element injector tree',
() => {
let componentInjector: Injector;
let moduleInjector: Injector;
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class MyComponent {
constructor(@SkipSelf() public injector: Injector) {
componentInjector = injector;
}
}
@NgModule({
declarations: [MyComponent],
providers: [{
provide: 'token',
useValue: 'NG_MODULE',
}]
})
class MyModule {
constructor(public injector: Injector) {
moduleInjector = injector;
}
}
TestBed.configureTestingModule({
imports: [MyModule],
});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
expect(componentInjector!.get('token')).toBe('NG_MODULE');
expect(moduleInjector!.get('token')).toBe('NG_MODULE');
});
it('should respect @Host in case @SkipSelf is used and no suitable Injector found in element injector tree',
() => {
let componentInjector: Injector;
let moduleInjector: Injector;
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class MyComponent {
constructor(@Host() @SkipSelf() public injector: Injector) {
componentInjector = injector;
}
}
@NgModule({
declarations: [MyComponent],
providers: [{
provide: 'token',
useValue: 'NG_MODULE',
}]
})
class MyModule {
constructor(public injector: Injector) {
moduleInjector = injector;
}
}
TestBed.configureTestingModule({
imports: [MyModule],
});
// If a token is injected with the @Host flag, the module injector is not searched
// for that token in Ivy.
if (ivyEnabled) {
expect(() => TestBed.createComponent(MyComponent))
.toThrowError(/NG0201: No provider for Injector found in NodeInjector/);
} else {
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
expect(componentInjector!.get('token')).toBe('NG_MODULE');
expect(moduleInjector!.get('token')).toBe('NG_MODULE');
}
});
it('should throw when injecting Injectors using @SkipSelf and @Host and no Injectors are available in a current view',
() => {
@Component({
selector: 'parent',
template: '',
providers: [{
provide: 'token',
useValue: 'PARENT',
}]
})
class ParentComponent {
}
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class ChildComponent {
constructor(@Host() @SkipSelf() public injector: Injector) {}
}
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
});
// Ivy has different error message when dependency is not found
const expectedErrorMessage = ivyEnabled ?
/NG0201: No provider for Injector found in NodeInjector/ :
/No provider for Injector/;
expect(() => TestBed.createComponent(ParentComponent))
.toThrowError(expectedErrorMessage);
});
it('should not throw when injecting Injectors using @SkipSelf, @Host, and @Optional and no Injectors are available in a current view',
() => {
@Component({
selector: 'parent',
template: '',
providers: [{
provide: 'token',
useValue: 'PARENT',
}]
})
class ParentComponent {
}
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class ChildComponent {
constructor(@Host() @SkipSelf() @Optional() public injector: Injector) {}
}
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
});
// Ivy has different error message when dependency is not found
const expectedErrorMessage = ivyEnabled ?
/NG0201: No provider for Injector found in NodeInjector/ :
/No provider for Injector/;
expect(() => TestBed.createComponent(ParentComponent))
.not.toThrowError(expectedErrorMessage);
});
});
describe('ElementRef', () => {
// While tokens like `ElementRef` make sense only in a context of a NodeInjector,
// ViewEngine also used `ModuleInjector` tree to lookup such tokens. In Ivy we replicate
// this behavior for now to avoid breaking changes.
it('should lookup module injector in case @SkipSelf is used for `ElementRef` token and Component has no parent',
() => {
let componentElement: ElementRef;
let moduleElement: ElementRef;
@Component({template: '
component
'})
class MyComponent {
constructor(@SkipSelf() public el: ElementRef) {
componentElement = el;
}
}
@NgModule({
declarations: [MyComponent],
providers: [{
provide: ElementRef,
useValue: {from: 'NG_MODULE'},
}]
})
class MyModule {
constructor(public el: ElementRef) {
moduleElement = el;
}
}
TestBed.configureTestingModule({
imports: [MyModule],
});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
expect((moduleElement! as any).from).toBe('NG_MODULE');
expect((componentElement! as any).from).toBe('NG_MODULE');
});
it('should return host node when @SkipSelf is used for `ElementRef` token and Component has no parent node',
() => {
let parentElement: ElementRef;
let componentElement: ElementRef;
@Component({selector: 'child', template: '...'})
class MyComponent {
constructor(@SkipSelf() public el: ElementRef) {
componentElement = el;
}
}
@Component({
template: '',
})
class ParentComponent {
constructor(public el: ElementRef) {
parentElement = el;
}
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [ParentComponent, MyComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(componentElement!).toEqual(parentElement!);
});
it('should @SkipSelf on child directive node when injecting ElementRef on nested parent directive',
() => {
let parentRef: ElementRef;
let childRef: ElementRef;
@Directive({selector: '[parent]'})
class ParentDirective {
constructor(elementRef: ElementRef) {
parentRef = elementRef;
}
}
@Directive({selector: '[child]'})
class ChildDirective {
constructor(@SkipSelf() elementRef: ElementRef) {
childRef = elementRef;
}
}
@Component({template: '
parent child
'})
class MyComp {
}
TestBed.configureTestingModule(
{declarations: [ParentDirective, ChildDirective, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
// Assert against the `nativeElement` since Ivy always returns a new ElementRef.
expect(childRef!.nativeElement).toBe(parentRef!.nativeElement);
expect(childRef!.nativeElement.tagName).toBe('DIV');
});
});
describe('@SkipSelf when parent contains embedded views', () => {
it('should work for `ElementRef` token', () => {
let requestedElementRef: ElementRef;
@Component({
selector: 'child',
template: '...',
})
class ChildComponent {
constructor(@SkipSelf() public elementRef: ElementRef) {
requestedElementRef = elementRef;
}
}
@Component({
selector: 'root',
template: '
',
})
class ParentComponent {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(requestedElementRef!.nativeElement).toEqual(fixture.nativeElement.firstChild);
expect(requestedElementRef!.nativeElement.tagName).toEqual('DIV');
});
it('should work for `ElementRef` token with expanded *ngIf', () => {
let requestedElementRef: ElementRef;
@Component({
selector: 'child',
template: '...',
})
class ChildComponent {
constructor(@SkipSelf() public elementRef: ElementRef) {
requestedElementRef = elementRef;
}
}
@Component({
selector: 'root',
template: '
',
})
class ParentComponent {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(requestedElementRef!.nativeElement).toEqual(fixture.nativeElement.firstChild);
expect(requestedElementRef!.nativeElement.tagName).toEqual('DIV');
});
it('should work for `ViewContainerRef` token', () => {
let requestedRef: ViewContainerRef;
@Component({
selector: 'child',
template: '...',
})
class ChildComponent {
constructor(@SkipSelf() public ref: ViewContainerRef) {
requestedRef = ref;
}
}
@Component({
selector: 'root',
template: '
',
})
class ParentComponent {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
if (ivyEnabled) {
expect(requestedRef!.element.nativeElement).toBe(fixture.nativeElement.firstChild);
expect(requestedRef!.element.nativeElement.tagName).toBe('DIV');
} else {
expect(requestedRef!).toBeNull();
}
});
it('should work for `ChangeDetectorRef` token', () => {
let requestedChangeDetectorRef: ChangeDetectorRef;
@Component({
selector: 'child',
template: '...',
})
class ChildComponent {
constructor(@SkipSelf() public changeDetectorRef: ChangeDetectorRef) {
requestedChangeDetectorRef = changeDetectorRef;
}
}
@Component({
selector: 'root',
template: '',
})
class ParentComponent {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
const {context} = requestedChangeDetectorRef! as ViewRefInternal;
expect(context).toBe(fixture.componentInstance);
});
// this works consistently between VE and Ivy
it('should work for Injectors', () => {
let childComponentInjector: Injector;
let parentComponentInjector: Injector;
@Component({
selector: 'parent',
template: '',
providers: [{
provide: 'token',
useValue: 'PARENT',
}]
})
class ParentComponent {
constructor(public injector: Injector) {
parentComponentInjector = injector;
}
}
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class ChildComponent {
constructor(@SkipSelf() public injector: Injector) {
childComponentInjector = injector;
}
}
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(childComponentInjector!.get('token'))
.toBe(parentComponentInjector!.get('token'));
});
it('should work for Injectors with expanded *ngIf', () => {
let childComponentInjector: Injector;
let parentComponentInjector: Injector;
@Component({
selector: 'parent',
template: '',
providers: [{
provide: 'token',
useValue: 'PARENT',
}]
})
class ParentComponent {
constructor(public injector: Injector) {
parentComponentInjector = injector;
}
}
@Component({
selector: 'child',
template: '...',
providers: [{
provide: 'token',
useValue: 'CHILD',
}]
})
class ChildComponent {
constructor(@SkipSelf() public injector: Injector) {
childComponentInjector = injector;
}
}
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
});
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(childComponentInjector!.get('token'))
.toBe(parentComponentInjector!.get('token'));
});
});
describe('TemplateRef', () => {
// SkipSelf doesn't make sense to use with TemplateRef since you
// can't inject TemplateRef on a regular element and you can initialize
// a child component on a nested `` only when a component/directive
// on a parent `` is initialized.
it('should throw when using @SkipSelf for TemplateRef', () => {
@Directive({selector: '[dir]', exportAs: 'dir'})
class MyDir {
constructor(@SkipSelf() public templateRef: TemplateRef) {}
}
@Component({selector: '[child]', template: ''})
class ChildComp {
constructor(public templateRef: TemplateRef) {}
@ViewChild(MyDir) directive!: MyDir;
}
@Component({
selector: 'root',
template: '',
})
class MyComp {
@ViewChild(ChildComp) child!: ChildComp;
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [MyDir, ChildComp, MyComp],
});
// Ivy has different error message when dependency is not found
const expectedErrorMessage = ivyEnabled ? /NG0201: No provider for TemplateRef found/ :
/No provider for TemplateRef/;
expect(() => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
}).toThrowError(expectedErrorMessage);
});
it('should throw when SkipSelf and no parent TemplateRef', () => {
@Directive({selector: '[dirA]', exportAs: 'dirA'})
class DirA {
constructor(@SkipSelf() public templateRef: TemplateRef) {}
}
@Component({
selector: 'root',
template: '',
})
class MyComp {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [DirA, MyComp],
});
// Ivy has different error message when dependency is not found
const expectedErrorMessage = ivyEnabled ? /NG0201: No provider for TemplateRef found/ :
/No provider for TemplateRef/;
expect(() => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
}).toThrowError(expectedErrorMessage);
});
it('should not throw when SkipSelf and Optional', () => {
let directiveTemplateRef;
@Directive({selector: '[dirA]', exportAs: 'dirA'})
class DirA {
constructor(@SkipSelf() @Optional() templateRef: TemplateRef) {
directiveTemplateRef = templateRef;
}
}
@Component({
selector: 'root',
template: '',
})
class MyComp {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [DirA, MyComp],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(directiveTemplateRef).toBeNull();
});
it('should not throw when SkipSelf, Optional, and Host', () => {
@Directive({selector: '[dirA]', exportAs: 'dirA'})
class DirA {
constructor(@SkipSelf() @Optional() @Host() public templateRef: TemplateRef) {}
}
@Component({
selector: 'root',
template: '',
})
class MyComp {
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [DirA, MyComp],
});
expect(() => TestBed.createComponent(MyComp)).not.toThrowError();
});
});
describe('ViewContainerRef', () => {
it('should support @SkipSelf when injecting ViewContainerRef', () => {
let parentViewContainer: ViewContainerRef;
let childViewContainer: ViewContainerRef;
@Directive({selector: '[parent]'})
class ParentDirective {
constructor(vc: ViewContainerRef) {
parentViewContainer = vc;
}
}
@Directive({selector: '[child]'})
class ChildDirective {
constructor(@SkipSelf() vc: ViewContainerRef) {
childViewContainer = vc;
}
}
@Component({template: '
parent child
'})
class MyComp {
}
TestBed.configureTestingModule(
{declarations: [ParentDirective, ChildDirective, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
// Assert against the `element` since Ivy always returns a new ViewContainerRef.
expect(childViewContainer!.element.nativeElement)
.toBe(parentViewContainer!.element.nativeElement);
expect(parentViewContainer!.element.nativeElement.tagName).toBe('DIV');
});
it('should get ViewContainerRef using @SkipSelf and @Host', () => {
let parentViewContainer: ViewContainerRef;
let childViewContainer: ViewContainerRef;
@Directive({selector: '[parent]'})
class ParentDirective {
constructor(vc: ViewContainerRef) {
parentViewContainer = vc;
}
}
@Directive({selector: '[child]'})
class ChildDirective {
constructor(@SkipSelf() @Host() vc: ViewContainerRef) {
childViewContainer = vc;
}
}
@Component({template: '
parent child
'})
class MyComp {
}
TestBed.configureTestingModule(
{declarations: [ParentDirective, ChildDirective, MyComp]});
if (ivyEnabled) {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
// Assert against the `element` since Ivy always returns a new ViewContainerRef.
expect(childViewContainer!.element.nativeElement)
.toBe(parentViewContainer!.element.nativeElement);
expect(parentViewContainer!.element.nativeElement.tagName).toBe('DIV');
} else {
// Template parse errors happen in VE
// "
parent [ERROR ->]child
"
expect(() => TestBed.createComponent(MyComp))
.toThrowError(/No provider for ViewContainerRef/);
}
});
it('should get ViewContainerRef using @SkipSelf and @Host on parent', () => {
let parentViewContainer: ViewContainerRef;
@Directive({selector: '[parent]'})
class ParentDirective {
constructor(@SkipSelf() vc: ViewContainerRef) {
parentViewContainer = vc;
}
}
@Component({template: '
parent
'})
class MyComp {
}
TestBed.configureTestingModule({declarations: [ParentDirective, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
if (ivyEnabled) {
// Assert against the `element` since Ivy always returns a new ViewContainerRef.
expect(parentViewContainer!.element.nativeElement.tagName).toBe('DIV');
} else {
// VE Doesn't throw, but the ref is null
expect(parentViewContainer!).toBeNull();
}
});
it('should throw when injecting ViewContainerRef using @SkipSelf and no ViewContainerRef are available in a current view',
() => {
@Component({template: 'component'})
class MyComp {
constructor(@SkipSelf() vc: ViewContainerRef) {}
}
TestBed.configureTestingModule({declarations: [MyComp]});
expect(() => TestBed.createComponent(MyComp))
.toThrowError(/No provider for ViewContainerRef/);
});
});
describe('ChangeDetectorRef', () => {
it('should support @SkipSelf when injecting ChangeDetectorRef', () => {
let parentRef: ChangeDetectorRef|undefined;
let childRef: ChangeDetectorRef|undefined;
@Directive({selector: '[parent]'})
class ParentDirective {
constructor(cdr: ChangeDetectorRef) {
parentRef = cdr;
}
}
@Directive({selector: '[child]'})
class ChildDirective {
constructor(@SkipSelf() cdr: ChangeDetectorRef) {
childRef = cdr;
}
}
@Component({template: '
parent child
'})
class MyComp {
}
TestBed.configureTestingModule(
{declarations: [ParentDirective, ChildDirective, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
// Assert against the `rootNodes` since Ivy always returns a new ChangeDetectorRef.
expect((parentRef as ViewRefInternal).rootNodes)
.toEqual((childRef as ViewRefInternal).rootNodes);
});
it('should inject host component ChangeDetectorRef when @SkipSelf', () => {
let childRef: ChangeDetectorRef|undefined;
@Component({selector: 'child', template: '...'})
class ChildComp {
constructor(@SkipSelf() cdr: ChangeDetectorRef) {
childRef = cdr;
}
}
@Component({template: '
'})
class MyComp {
constructor(public cdr: ChangeDetectorRef) {}
}
TestBed.configureTestingModule({declarations: [ChildComp, MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
// Assert against the `rootNodes` since Ivy always returns a new ChangeDetectorRef.
expect((childRef as ViewRefInternal).rootNodes)
.toEqual((fixture.componentInstance.cdr as ViewRefInternal).rootNodes);
});
it('should throw when ChangeDetectorRef and @SkipSelf and not found', () => {
@Component({template: ''})
class MyComponent {
constructor(@SkipSelf() public injector: ChangeDetectorRef) {}
}
@NgModule({
declarations: [MyComponent],
})
class MyModule {
}
TestBed.configureTestingModule({
imports: [MyModule],
});
expect(() => TestBed.createComponent(MyComponent))
.toThrowError(/No provider for ChangeDetectorRef/);
});
it('should lookup module injector in case @SkipSelf is used for `ChangeDetectorRef` token and Component has no parent',
() => {
let componentCDR: ChangeDetectorRef;
let moduleCDR: ChangeDetectorRef;
@Component({selector: 'child', template: '...'})
class MyComponent {
constructor(@SkipSelf() public injector: ChangeDetectorRef) {
componentCDR = injector;
}
}
@NgModule({
declarations: [MyComponent],
providers: [{
provide: ChangeDetectorRef,
useValue: {from: 'NG_MODULE'},
}]
})
class MyModule {
constructor(public injector: ChangeDetectorRef) {
moduleCDR = injector;
}
}
TestBed.configureTestingModule({
imports: [MyModule],
});
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
expect((moduleCDR! as any).from).toBe('NG_MODULE');
expect((componentCDR! as any).from).toBe('NG_MODULE');
});
});
describe('viewProviders', () => {
it('should support @SkipSelf when using viewProviders', () => {
@Component({
selector: 'child',
template: '{{ blah | json }} {{ foo | json }} {{ bar | json }}',
providers: [{provide: 'Blah', useValue: 'Blah as Provider'}],
viewProviders: [
{provide: 'Foo', useValue: 'Foo as ViewProvider'},
{provide: 'Bar', useValue: 'Bar as ViewProvider'},
]
})
class Child {
constructor(
@Inject('Blah') public blah: String,
@Inject('Foo') public foo: String,
@SkipSelf() @Inject('Bar') public bar: String,
) {}
}
@Component({
selector: 'parent',
template: '',
providers: [
{provide: 'Blah', useValue: 'Blah as provider'},
{provide: 'Bar', useValue: 'Bar as Provider'},
],
viewProviders: [
{provide: 'Foo', useValue: 'Foo as ViewProvider'},
{provide: 'Bar', useValue: 'Bar as ViewProvider'},
]
})
class Parent {
}
@Component({selector: 'my-app', template: ''})
class MyApp {
@ViewChild(Parent) parent!: Parent;
@ViewChild(Child) child!: Child;
}
TestBed.configureTestingModule({declarations: [Child, Parent, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const child = fixture.componentInstance.child;
if (ivyEnabled) {
expect(child.bar).toBe('Bar as Provider');
} else {
// this seems like a ViewEngine bug
expect(child.bar).toBe('Bar as ViewProvider');
}
});
it('should throw when @SkipSelf and no accessible viewProvider', () => {
@Component({
selector: 'child',
template: '{{ blah | json }} {{ foo | json }} {{ bar | json }}',
providers: [{provide: 'Blah', useValue: 'Blah as Provider'}],
viewProviders: [
{provide: 'Foo', useValue: 'Foo as ViewProvider'},
{provide: 'Bar', useValue: 'Bar as ViewProvider'},
]
})
class Child {
constructor(
@Inject('Blah') public blah: String,
@Inject('Foo') public foo: String,
@SkipSelf() @Inject('Bar') public bar: String,
) {}
}
@Component({
selector: 'parent',
template: '',
providers: [{provide: 'Blah', useValue: 'Blah as provider'}],
viewProviders: [
{provide: 'Foo', useValue: 'Foo as ViewProvider'},
{provide: 'Bar', useValue: 'Bar as ViewProvider'},
]
})
class Parent {
}
@Component({selector: 'my-app', template: ''})
class MyApp {
}
TestBed.configureTestingModule({declarations: [Child, Parent, MyApp]});
expect(() => TestBed.createComponent(MyApp)).toThrowError(/No provider for Bar/);
});
it('should not throw when @SkipSelf and @Optional with no accessible viewProvider',
() => {
@Component({
selector: 'child',
template: '{{ blah | json }} {{ foo | json }} {{ bar | json }}',
providers: [{provide: 'Blah', useValue: 'Blah as Provider'}],
viewProviders: [
{provide: 'Foo', useValue: 'Foo as ViewProvider'},
{provide: 'Bar', useValue: 'Bar as ViewProvider'},
]
})
class Child {
constructor(
@Inject('Blah') public blah: String,
@Inject('Foo') public foo: String,
@SkipSelf() @Optional() @Inject('Bar') public bar: String,
) {}
}
@Component({
selector: 'parent',
template: '',
providers: [{provide: 'Blah', useValue: 'Blah as provider'}],
viewProviders: [
{provide: 'Foo', useValue: 'Foo as ViewProvider'},
{provide: 'Bar', useValue: 'Bar as ViewProvider'},
]
})
class Parent {
}
@Component({selector: 'my-app', template: ''})
class MyApp {
}
TestBed.configureTestingModule({declarations: [Child, Parent, MyApp]});
expect(() => TestBed.createComponent(MyApp)).not.toThrowError(/No provider for Bar/);
});
});
});
describe('@Host', () => {
@Directive({selector: '[dirA]'})
class DirectiveA {
constructor(@Host() public dirB: DirectiveB) {}
}
@Directive({selector: '[dirString]'})
class DirectiveString {
constructor(@Host() public s: String) {}
}
it('should find viewProviders on the host itself', () => {
@Component({
selector: 'my-comp',
template: '',
viewProviders: [{provide: String, useValue: 'Foo'}]
})
class MyComp {
@ViewChild(DirectiveString) dirString!: DirectiveString;
}
@Component({template: ''})
class MyApp {
@ViewChild(MyComp) myComp!: MyComp;
}
TestBed.configureTestingModule({declarations: [DirectiveString, MyComp, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const dirString = fixture.componentInstance.myComp.dirString;
expect(dirString.s).toBe('Foo');
});
it('should find host component on the host itself', () => {
@Directive({selector: '[dirComp]'})
class DirectiveComp {
constructor(@Inject(forwardRef(() => MyComp)) @Host() public comp: MyComp) {}
}
@Component({selector: 'my-comp', template: ''})
class MyComp {
@ViewChild(DirectiveComp) dirComp!: DirectiveComp;
}
@Component({template: ''})
class MyApp {
@ViewChild(MyComp) myComp!: MyComp;
}
TestBed.configureTestingModule({declarations: [DirectiveComp, MyComp, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const myComp = fixture.componentInstance.myComp;
const dirComp = myComp.dirComp;
expect(dirComp.comp).toBe(myComp);
});
onlyInIvy('Ivy has different error message when dependency is not found')
.it('should not find providers on the host itself', () => {
@Component({
selector: 'my-comp',
template: '',
providers: [{provide: String, useValue: 'Foo'}]
})
class MyComp {
}
@Component({template: ''})
class MyApp {
}
TestBed.configureTestingModule({declarations: [DirectiveString, MyComp, MyApp]});
expect(() => TestBed.createComponent(MyApp))
.toThrowError(
'NG0201: No provider for String found in NodeInjector. Find more at https://angular.io/errors/NG0201');
});
onlyInIvy('Ivy has different error message when dependency is not found')
.it('should not find other directives on the host itself', () => {
@Component({selector: 'my-comp', template: ''})
class MyComp {
}
@Component({template: ''})
class MyApp {
}
TestBed.configureTestingModule(
{declarations: [DirectiveA, DirectiveB, MyComp, MyApp]});
expect(() => TestBed.createComponent(MyApp))
.toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/);
});
onlyInIvy('Ivy has different error message when dependency is not found')
.it('should not find providers on the host itself if in inline view', () => {
@Component({
selector: 'my-comp',
template: ''
})
class MyComp {
showing = false;
}
@Component({template: ''})
class MyApp {
@ViewChild(MyComp) myComp!: MyComp;
}
TestBed.configureTestingModule(
{declarations: [DirectiveA, DirectiveB, MyComp, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(() => {
fixture.componentInstance.myComp.showing = true;
fixture.detectChanges();
}).toThrowError(/NG0201: No provider for DirectiveB found in NodeInjector/);
});
it('should find providers across embedded views if not passing component boundary', () => {
@Component({template: '
'})
class MyApp {
showing = false;
@ViewChild(DirectiveA) dirA!: DirectiveA;
@ViewChild(DirectiveB) dirB!: DirectiveB;
}
TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
fixture.componentInstance.showing = true;
fixture.detectChanges();
const dirA = fixture.componentInstance.dirA;
const dirB = fixture.componentInstance.dirB;
expect(dirA.dirB).toBe(dirB);
});
onlyInIvy('Ivy has different error message when dependency is not found')
.it('should not find component above the host', () => {
@Directive({selector: '[dirComp]'})
class DirectiveComp {
constructor(@Inject(forwardRef(() => MyApp)) @Host() public comp: MyApp) {}
}
@Component({selector: 'my-comp', template: ''})
class MyComp {
}
@Component({template: ''})
class MyApp {
}
TestBed.configureTestingModule({declarations: [DirectiveComp, MyComp, MyApp]});
expect(() => TestBed.createComponent(MyApp))
.toThrowError(
'NG0201: No provider for MyApp found in NodeInjector. Find more at https://angular.io/errors/NG0201');
});
describe('regression', () => {
// based on https://stackblitz.com/edit/angular-riss8k?file=src/app/app.component.ts
it('should allow directives with Host flag to inject view providers from containing component',
() => {
class ControlContainer {}
let controlContainers: ControlContainer[] = [];
let injectedControlContainer: ControlContainer|null = null;
@Directive({
selector: '[group]',
providers: [{provide: ControlContainer, useExisting: GroupDirective}]
})
class GroupDirective {
constructor() {
controlContainers.push(this);
}
}
@Directive({selector: '[control]'})
class ControlDirective {
constructor(@Host() @SkipSelf() @Inject(ControlContainer) parent:
ControlContainer) {
injectedControlContainer = parent;
}
}
@Component({
selector: 'my-comp',
template: '',
viewProviders: [{provide: ControlContainer, useExisting: GroupDirective}]
})
class MyComp {
}
@Component({
template: `