diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index b46bef621a..7adccac568 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 17210, + "main-es2015": 17597, "polyfills-es2015": 36709 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 146698, + "main-es2015": 147252, "polyfills-es2015": 36964 } } @@ -30,7 +30,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 136062, + "main-es2015": 136703, "polyfills-es2015": 37641 } } @@ -66,4 +66,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 54085071a8..5e0fc857df 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -27,7 +27,7 @@ import {AttributeMarker, TContainerNode, TDirectiveHostNode, TElementContainerNo import {isComponentDef, isComponentHost} from './interfaces/type_checks'; import {DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, INJECTOR, LView, T_HOST, TData, TVIEW, TView, TViewType} from './interfaces/view'; import {assertTNodeType} from './node_assert'; -import {enterDI, leaveDI} from './state'; +import {enterDI, getCurrentTNode, getLView, leaveDI} from './state'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {stringifyForError} from './util/misc_utils'; @@ -345,6 +345,51 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str } +function notFoundValueOrThrow( + notFoundValue: T|null, token: Type|InjectionToken, flags: InjectFlags): T|null { + if (flags & InjectFlags.Optional) { + return notFoundValue; + } else { + throwProviderNotFoundError(token, 'NodeInjector'); + } +} + +/** + * Returns the value associated to the given token from the ModuleInjector or throws exception + * + * @param lView The `LView` that contains the `tNode` + * @param token The token to look for + * @param flags Injection flags + * @param notFoundValue The value to return when the injection flags is `InjectFlags.Optional` + * @returns the value from the injector or throws an exception + */ +function lookupTokenUsingModuleInjector( + lView: LView, token: Type|InjectionToken, flags: InjectFlags, notFoundValue?: any): T| + null { + if (flags & InjectFlags.Optional && notFoundValue === undefined) { + // This must be set or the NullInjector will throw for optional deps + notFoundValue = null; + } + + if ((flags & (InjectFlags.Self | InjectFlags.Host)) === 0) { + const moduleInjector = lView[INJECTOR]; + // switch to `injectInjectorOnly` implementation for module injector, since module injector + // should not have access to Component/Directive DI scope (that may happen through + // `directiveInject` implementation) + const previousInjectImplementation = setInjectImplementation(undefined); + try { + if (moduleInjector) { + return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional); + } else { + return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional); + } + } finally { + setInjectImplementation(previousInjectImplementation); + } + } + return notFoundValueOrThrow(notFoundValue, token, flags); +} + /** * Returns the value associated to the given token from the NodeInjectors => ModuleInjector. * @@ -352,8 +397,7 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str * the module injector tree. * * This function patches `token` with `__NG_ELEMENT_ID__` which contains the id for the bloom - * filter. Negative values are reserved for special objects. - * - `-1` is reserved for injecting `Injector` (implemented by `NodeInjector`) + * filter. `-1` is reserved for injecting `Injector` (implemented by `NodeInjector`) * * @param tNode The Node where the search for the injector should start * @param lView The `LView` that contains the `tNode` @@ -370,7 +414,10 @@ export function getOrCreateInjectable( // If the ID stored here is a function, this is a special object like ElementRef or TemplateRef // so just call the factory function to create it. if (typeof bloomHash === 'function') { - enterDI(lView, tNode); + if (!enterDI(lView, tNode, flags)) { + // Failed to enter DI use module injector instead. + return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); + } try { const value = bloomHash(); if (value == null && !(flags & InjectFlags.Optional)) { @@ -381,10 +428,30 @@ export function getOrCreateInjectable( } finally { leaveDI(); } - } else if (typeof bloomHash == 'number') { + } else if (typeof bloomHash === 'number') { + // This is a value used to identify __NG_ELEMENT_ID__ + // `-1` is a special value used to identify `Injector` types in NodeInjector + // This is a workaround for the fact that if the `Injector.__NG_ELEMENT_ID__` + // would have a factory function (such as `ElementRef`) it would cause Ivy + // to be pulled into the ViewEngine, because they both share `Injector` type. + // This should be refactored to follow `ElementRef` pattern once ViewEngine is + // removed if (bloomHash === -1) { - // `-1` is a special value used to identify `Injector` types. - return new NodeInjector(tNode, lView) as any; + if (!enterDI(lView, tNode, flags)) { + // Failed to enter DI, try module injector instead. If a token is injected with the @Host + // flag, the module injector is not searched for that token in Ivy. + return (flags & InjectFlags.Host) ? + notFoundValueOrThrow(notFoundValue, token, flags) : + lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); + } + try { + // Retrieving current `TNode` and `LView` from the state (rather than using `tNode` and + // `lView`), because entering DI (by calling `enterDI`) may cause these values to change + // (in case `@SkipSelf` flag is present). + return new NodeInjector(getCurrentTNode()! as TDirectiveHostNode, getLView()) as any; + } finally { + leaveDI(); + } } // If the token has a bloom hash, then it is a token which could be in NodeInjector. @@ -453,32 +520,7 @@ export function getOrCreateInjectable( } } - if (flags & InjectFlags.Optional && notFoundValue === undefined) { - // This must be set or the NullInjector will throw for optional deps - notFoundValue = null; - } - - if ((flags & (InjectFlags.Self | InjectFlags.Host)) === 0) { - const moduleInjector = lView[INJECTOR]; - // switch to `injectInjectorOnly` implementation for module injector, since module injector - // should not have access to Component/Directive DI scope (that may happen through - // `directiveInject` implementation) - const previousInjectImplementation = setInjectImplementation(undefined); - try { - if (moduleInjector) { - return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional); - } else { - return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional); - } - } finally { - setInjectImplementation(previousInjectImplementation); - } - } - if (flags & InjectFlags.Optional) { - return notFoundValue; - } else { - throwProviderNotFoundError(token, 'NodeInjector'); - } + return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); } const NOT_FOUND = {}; @@ -582,7 +624,11 @@ export function getNodeInjectable( factory.resolving = true; const previousInjectImplementation = factory.injectImpl ? setInjectImplementation(factory.injectImpl) : null; - enterDI(lView, tNode); + const success = enterDI(lView, tNode, InjectFlags.Default); + ngDevMode && + assertEqual( + success, true, + 'Because flags do not contain \`SkipSelf\' we expect this to always succeed.'); try { value = lView[index] = factory.factory(undefined, tData, lView, tNode); // This code path is hit for both directives and providers. diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index d4e971dcf6..0217e2f60d 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {InjectFlags} from '../di/interface/injector'; import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertLessThan, assertNotEqual} from '../util/assert'; -import {assertLViewOrUndefined, assertTNodeForTView} from './assert'; +import {assertLViewOrUndefined, assertTNodeForLView, assertTNodeForTView} from './assert'; import {DirectiveDef} from './interfaces/definition'; import {TNode, TNodeType} from './interfaces/node'; -import {CONTEXT, DECLARATION_VIEW, HEADER_OFFSET, LView, OpaqueViewState, TData, TVIEW, TView} from './interfaces/view'; +import {CONTEXT, DECLARATION_VIEW, HEADER_OFFSET, LView, OpaqueViewState, T_HOST, TData, TVIEW, TView, TViewType} from './interfaces/view'; import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces'; -import {assertTNodeType} from './node_assert'; import {getTNode} from './util/view_utils'; @@ -434,16 +434,89 @@ export function setCurrentQueryIndex(value: number): void { } /** - * This is a light weight version of the `enterView` which is needed by the DI system. - * @param newView - * @param tNode + * Returns a `TNode` of the location where the current `LView` is declared at. + * + * @param lView an `LView` that we want to find parent `TNode` for. */ -export function enterDI(newView: LView, tNode: TNode) { - ngDevMode && assertLViewOrUndefined(newView); - const newLFrame = allocLFrame(); - instructionState.lFrame = newLFrame; - newLFrame.currentTNode = tNode!; - newLFrame.lView = newView; +function getDeclarationTNode(lView: LView): TNode|null { + const tView = lView[TVIEW]; + + // Return the declaration parent for embedded views + if (tView.type === TViewType.Embedded) { + ngDevMode && assertDefined(tView.declTNode, 'Embedded TNodes should have declaration parents.'); + return tView.declTNode; + } + + // Components don't have `TView.declTNode` because each instance of component could be + // inserted in different location, hence `TView.declTNode` is meaningless. + // Falling back to `T_HOST` in case we cross component boundary. + if (tView.type === TViewType.Component) { + return lView[T_HOST]; + } + + // Remaining TNode type is `TViewType.Root` which doesn't have a parent TNode. + return null; +} + +/** + * This is a light weight version of the `enterView` which is needed by the DI system. + * + * @param lView `LView` location of the DI context. + * @param tNode `TNode` for DI context + * @param flags DI context flags. if `SkipSelf` flag is set than we walk up the declaration + * tree from `tNode` until we find parent declared `TElementNode`. + * @returns `true` if we have successfully entered DI associated with `tNode` (or with declared + * `TNode` if `flags` has `SkipSelf`). Failing to enter DI implies that no associated + * `NodeInjector` can be found and we should instead use `ModuleInjector`. + * - If `true` than this call must be fallowed by `leaveDI` + * - If `false` than this call failed and we should NOT call `leaveDI` + */ +export function enterDI(lView: LView, tNode: TNode, flags: InjectFlags) { + ngDevMode && assertLViewOrUndefined(lView); + + if (flags & InjectFlags.SkipSelf) { + ngDevMode && assertTNodeForTView(tNode, lView[TVIEW]); + + let parentTNode = tNode as TNode | null; + let parentLView = lView; + + while (true) { + ngDevMode && assertDefined(parentTNode, 'Parent TNode should be defined'); + parentTNode = parentTNode!.parent as TNode | null; + if (parentTNode === null && !(flags & InjectFlags.Host)) { + parentTNode = getDeclarationTNode(parentLView); + if (parentTNode === null) break; + + // In this case, a parent exists and is definitely an element. So it will definitely + // have an existing lView as the declaration view, which is why we can assume it's defined. + ngDevMode && assertDefined(parentLView, 'Parent LView should be defined'); + parentLView = parentLView[DECLARATION_VIEW]!; + + // In Ivy there are Comment nodes that correspond to ngIf and NgFor embedded directives + // We want to skip those and look only at Elements and ElementContainers to ensure + // we're looking at true parent nodes, and not content or other types. + if (parentTNode.type & (TNodeType.Element | TNodeType.ElementContainer)) { + break; + } + } else { + break; + } + } + if (parentTNode === null) { + // If we failed to find a parent TNode this means that we should use module injector. + return false; + } else { + tNode = parentTNode; + lView = parentLView; + } + } + + ngDevMode && assertTNodeForLView(tNode, lView); + const lFrame = instructionState.lFrame = allocLFrame(); + lFrame.currentTNode = tNode; + lFrame.lView = lView; + + return true; } /** diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 768fbf5827..820416d60e 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -7,10 +7,11 @@ */ import {CommonModule} from '@angular/common'; -import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, 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 {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'; @@ -700,30 +701,6 @@ describe('di', () => { }); }); - it('should skip the current node with @SkipSelf', () => { - @Directive({selector: '[dirA]'}) - class DirectiveA { - constructor(@SkipSelf() public dirB: DirectiveB) {} - } - - @Component({selector: 'my-comp', template: '
'}) - class MyComp { - @ViewChild(DirectiveA) dirA!: DirectiveA; - } - - @Component({template: ''}) - class MyApp { - @ViewChild(MyComp) myComp!: MyComp; - } - - TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); - const fixture = TestBed.createComponent(MyApp); - fixture.detectChanges(); - - const dirA = fixture.componentInstance.myComp.dirA; - expect(dirA.dirB.value).toEqual('parent'); - }); - onlyInIvy('Ivy has different error message when dependency is not found') .it('should check only the current node with @Self', () => { @Directive({selector: '[dirA]'}) @@ -739,6 +716,994 @@ describe('di', () => { .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 { @@ -1134,7 +2099,8 @@ describe('di', () => { expect(provider!.getMessage()).toBe('bar'); - // ViewEngine incorrectly uses the original class DI config, instead of the one from useClass. + // ViewEngine incorrectly uses the original class DI config, instead of the one from + // useClass. if (ivyEnabled) { expect(provider!.dep.name).toBe('BarServiceDep'); } @@ -1180,7 +2146,8 @@ describe('di', () => { expect(directProvider!.getMessage()).toBe('bar'); expect(overriddenProvider!.getMessage()).toBe('foo'); - // ViewEngine incorrectly uses the original class DI config, instead of the one from useClass. + // ViewEngine incorrectly uses the original class DI config, instead of the one from + // useClass. if (ivyEnabled) { expect(directProvider!.dep.name).toBe('BarServiceDep'); expect(overriddenProvider!.dep.name).toBe('FooServiceDep'); @@ -1811,8 +2778,9 @@ describe('di', () => { providers: [{ provide: LOCALE_ID, useFactory: () => 'ja-JP', - // Note: `LOCALE_ID` is also provided within APPLICATION_MODULE_PROVIDERS, so we use it here - // as a dep and making sure it doesn't cause cyclic dependency (since @SkipSelf is present) + // Note: `LOCALE_ID` is also provided within APPLICATION_MODULE_PROVIDERS, so we use it + // here as a dep and making sure it doesn't cause cyclic dependency (since @SkipSelf is + // present) deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]] }] }) diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index cc440fb4aa..d4f067f254 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -14,6 +14,9 @@ { "name": "EMPTY_OBJ" }, + { + "name": "InjectFlags" + }, { "name": "Module" }, @@ -173,6 +176,9 @@ { "name": "getCurrentTNodePlaceholderOk" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFirstLContainer" }, diff --git a/packages/core/test/bundling/forms/bundle.golden_symbols.json b/packages/core/test/bundling/forms/bundle.golden_symbols.json index 0e06d058e6..4e692ada58 100644 --- a/packages/core/test/bundling/forms/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms/bundle.golden_symbols.json @@ -983,6 +983,9 @@ { "name": "getDebugContext" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFactoryDef" }, @@ -1286,6 +1289,9 @@ { "name": "localeEn" }, + { + "name": "lookupTokenUsingModuleInjector" + }, { "name": "makeParamDecorator" }, @@ -1379,6 +1385,9 @@ { "name": "normalizeValidators" }, + { + "name": "notFoundValueOrThrow" + }, { "name": "observable" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index dd2c07f81c..6f5521b982 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -11,6 +11,9 @@ { "name": "EMPTY_OBJ" }, + { + "name": "InjectFlags" + }, { "name": "NG_COMP_DEF" }, @@ -128,6 +131,9 @@ { "name": "getCurrentTNodePlaceholderOk" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFirstLContainer" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 40f40ecf01..5daed97b13 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1292,6 +1292,9 @@ { "name": "getDebugContext" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFactoryDef" }, @@ -1607,6 +1610,9 @@ { "name": "locateDirectiveOrProvider" }, + { + "name": "lookupTokenUsingModuleInjector" + }, { "name": "makeParamDecorator" }, @@ -1703,6 +1709,9 @@ { "name": "normalizeQueryParams" }, + { + "name": "notFoundValueOrThrow" + }, { "name": "observable" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 197a374cf8..c053fb45b0 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -353,6 +353,9 @@ { "name": "getDebugContext" }, + { + "name": "getDeclarationTNode" + }, { "name": "getFirstLContainer" }, @@ -542,6 +545,9 @@ { "name": "leaveViewLight" }, + { + "name": "lookupTokenUsingModuleInjector" + }, { "name": "makeParamDecorator" }, @@ -587,6 +593,9 @@ { "name": "noSideEffects" }, + { + "name": "notFoundValueOrThrow" + }, { "name": "readPatchedData" },