diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index 831d76a498a2c..87c4b25df1e39 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -355,6 +355,12 @@ export function resolveDep( } const tokenKey = depDef.tokenKey; + if (tokenKey === ChangeDetectorRefTokenKey) { + // directives on the same element as a component should be able to control the change detector + // of that component as well. + allowPrivateServices = !!(elDef && elDef.element !.componentView); + } + if (elDef && (depDef.flags & DepFlags.SkipSelf)) { allowPrivateServices = false; elDef = elDef.parent !; diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 54127c8e64441..fac2adef91807 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -90,7 +90,9 @@ class ComponentFactory_ extends ComponentFactory { const view = Services.createRootView( injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); const component = asProviderData(view, componentNodeIndex).instance; - view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); + if (rootSelectorOrNode) { + view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); + } return new ComponentRef_(view, new ViewRef_(view), component); } @@ -236,11 +238,11 @@ export class ViewRef_ implements EmbeddedViewRef, InternalViewRef { get destroyed(): boolean { return (this._view.state & ViewState.Destroyed) !== 0; } markForCheck(): void { markParentViewsForCheck(this._view); } - detach(): void { this._view.state &= ~ViewState.ChecksEnabled; } + detach(): void { this._view.state &= ~ViewState.Attached; } detectChanges(): void { Services.checkAndUpdateView(this._view); } checkNoChanges(): void { Services.checkNoChangesView(this._view); } - reattach(): void { this._view.state |= ViewState.ChecksEnabled; } + reattach(): void { this._view.state |= ViewState.Attached; } onDestroy(callback: Function) { if (!this._view.disposables) { this._view.disposables = []; diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index 325f75faad9f0..104f3c7c9dc47 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -324,9 +324,12 @@ export interface ViewData { */ export const enum ViewState { FirstCheck = 1 << 0, - ChecksEnabled = 1 << 1, - Errored = 1 << 2, - Destroyed = 1 << 3 + Attached = 1 << 1, + ChecksEnabled = 1 << 2, + Errored = 1 << 3, + Destroyed = 1 << 4, + + CatDetectChanges = Attached | ChecksEnabled, } export interface DisposableFn { (): void; } diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index 4150c29be352e..44f36d61902f3 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -211,7 +211,7 @@ function createView( viewContainerParent: null, parentNodeDef, context: null, component: null, nodes, - state: ViewState.FirstCheck | ViewState.ChecksEnabled, root, renderer, + state: ViewState.FirstCheck | ViewState.CatDetectChanges, root, renderer, oldValues: new Array(def.bindingCount), disposables }; return view; @@ -542,13 +542,13 @@ function callViewAction(view: ViewData, action: ViewAction) { const viewState = view.state; switch (action) { case ViewAction.CheckNoChanges: - if ((viewState & ViewState.ChecksEnabled) && + if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges && (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { checkNoChangesView(view); } break; case ViewAction.CheckAndUpdate: - if ((viewState & ViewState.ChecksEnabled) && + if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges && (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { checkAndUpdateView(view); } diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 8a1839c0375be..fe6444071c6a8 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -1175,6 +1175,21 @@ export function main() { expect(renderLog.log).toEqual([]); })); + it('Detached should disable OnPush', fakeAsync(() => { + const ctx = createCompFixture(''); + ctx.componentInstance.value = 0; + ctx.detectChanges(); + renderLog.clear(); + + const cmp: CompWithRef = queryDirs(ctx.debugElement, PushComp)[0]; + cmp.changeDetectorRef.detach(); + + ctx.componentInstance.value = 1; + ctx.detectChanges(); + + expect(renderLog.log).toEqual([]); + })); + it('Detached view can be checked locally', fakeAsync(() => { const ctx = createCompFixture(''); const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0]; @@ -1225,7 +1240,6 @@ export function main() { ctx.detectChanges(); expect(cmp.renderCount).toBe(count); - })); }); diff --git a/packages/core/test/linker/regression_integration_spec.ts b/packages/core/test/linker/regression_integration_spec.ts index dd11db580ac3f..7a65d3f7f2379 100644 --- a/packages/core/test/linker/regression_integration_spec.ts +++ b/packages/core/test/linker/regression_integration_spec.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ContentChild, Directive, InjectionToken, Injector, Input, Pipe, PipeTransform, Provider, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChildren, ViewContainerRef} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ContentChild, Directive, InjectionToken, Injector, Input, NgModule, NgModuleRef, Pipe, PipeTransform, Provider, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed, fakeAsync, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {expect} from '@angular/platform-browser/testing/src/matchers'; export function main() { @@ -365,6 +366,23 @@ function declareTests({useJit}: {useJit: boolean}) { expect(testDirs[1].tpl).toBeDefined(); expect(testDirs[2].tpl).toBeDefined(); }); + + it('should not add ng-version for dynamically created components', () => { + @Component({template: ''}) + class App { + } + + @NgModule({declarations: [App], entryComponents: [App]}) + class MyModule { + } + + const modRef = TestBed.configureTestingModule({imports: [MyModule]}) + .get(NgModuleRef) as NgModuleRef; + const compRef = + modRef.componentFactoryResolver.resolveComponentFactory(App).create(Injector.NULL); + + expect(getDOM().hasAttribute(compRef.location.nativeElement, 'ng-version')).toBe(false); + }); }); } diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index 77359bc2c9b27..76900b100c674 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -652,6 +652,46 @@ export function main() { expect(compEl.nativeElement).toHaveText('1'); }); + it('should inject ChangeDetectorRef of a same element component into a directive', () => { + TestBed.configureTestingModule( + {declarations: [PushComponentNeedsChangeDetectorRef, DirectiveNeedsChangeDetectorRef]}); + const cf = createComponentFixture( + '
'); + cf.detectChanges(); + const compEl = cf.debugElement.children[0]; + const comp = compEl.injector.get(PushComponentNeedsChangeDetectorRef); + const dir = compEl.injector.get(DirectiveNeedsChangeDetectorRef); + comp.counter = 1; + cf.detectChanges(); + expect(compEl.nativeElement).toHaveText('0'); + dir.changeDetectorRef.markForCheck(); + cf.detectChanges(); + expect(compEl.nativeElement).toHaveText('1'); + }); + + it(`should not inject ChangeDetectorRef of a parent element's component into a directive`, () => { + TestBed + .configureTestingModule({ + declarations: [PushComponentNeedsChangeDetectorRef, DirectiveNeedsChangeDetectorRef] + }) + .overrideComponent( + PushComponentNeedsChangeDetectorRef, + {set: {template: '{{counter}}'}}); + const cf = createComponentFixture( + '
'); + cf.detectChanges(); + const compEl = cf.debugElement.children[0]; + const comp = compEl.injector.get(PushComponentNeedsChangeDetectorRef); + const dirEl = compEl.children[0]; + const dir = dirEl.injector.get(DirectiveNeedsChangeDetectorRef); + comp.counter = 1; + cf.detectChanges(); + expect(compEl.nativeElement).toHaveText('0'); + dir.changeDetectorRef.markForCheck(); + cf.detectChanges(); + expect(compEl.nativeElement).toHaveText('0'); + }); + it('should inject ViewContainerRef', () => { TestBed.configureTestingModule({declarations: [NeedsViewContainerRef]}); const el = createComponent('
');