diff --git a/modules/@angular/core/src/application_ref.ts b/modules/@angular/core/src/application_ref.ts index 078104d6ab..3defc16114 100644 --- a/modules/@angular/core/src/application_ref.ts +++ b/modules/@angular/core/src/application_ref.ts @@ -21,6 +21,8 @@ import {CompilerFactory, CompilerOptions} from './linker/compiler'; import {ComponentFactory, ComponentRef} from './linker/component_factory'; import {ComponentFactoryResolver} from './linker/component_factory_resolver'; import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory'; +import {AppView} from './linker/view'; +import {ViewRef, ViewRef_} from './linker/view_ref'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; import {Testability, TestabilityRegistry} from './testability/testability'; import {Type} from './type'; @@ -387,6 +389,23 @@ export abstract class ApplicationRef { * Get a list of components registered to this application. */ get components(): ComponentRef[] { return []>unimplemented(); }; + + /** + * Attaches a view so that it will be dirty checked. + * The view will be automatically detached when it is destroyed. + * This will throw if the view is already attached to a ViewContainer. + */ + attachView(view: ViewRef): void { unimplemented(); } + + /** + * Detaches a view from dirty checking again. + */ + detachView(view: ViewRef): void { unimplemented(); } + + /** + * Returns the number of attached views. + */ + get viewCount() { return unimplemented(); } } @Injectable() @@ -397,7 +416,7 @@ export class ApplicationRef_ extends ApplicationRef { private _bootstrapListeners: Function[] = []; private _rootComponents: ComponentRef[] = []; private _rootComponentTypes: Type[] = []; - private _changeDetectorRefs: ChangeDetectorRef[] = []; + private _views: AppView[] = []; private _runningTick: boolean = false; private _enforceNoNewChanges: boolean = false; @@ -415,12 +434,16 @@ export class ApplicationRef_ extends ApplicationRef { {next: () => { this._zone.run(() => { this.tick(); }); }}); } - registerChangeDetector(changeDetector: ChangeDetectorRef): void { - this._changeDetectorRefs.push(changeDetector); + attachView(viewRef: ViewRef): void { + const view = (viewRef as ViewRef_).internalView; + this._views.push(view); + view.attachToAppRef(this); } - unregisterChangeDetector(changeDetector: ChangeDetectorRef): void { - ListWrapper.remove(this._changeDetectorRefs, changeDetector); + detachView(viewRef: ViewRef): void { + const view = (viewRef as ViewRef_).internalView; + ListWrapper.remove(this._views, view); + view.detach(); } bootstrap(componentOrFactory: ComponentFactory|Type): ComponentRef { @@ -451,9 +474,8 @@ export class ApplicationRef_ extends ApplicationRef { return compRef; } - /** @internal */ - _loadComponent(componentRef: ComponentRef): void { - this._changeDetectorRefs.push(componentRef.changeDetectorRef); + private _loadComponent(componentRef: ComponentRef): void { + this.attachView(componentRef.hostView); this.tick(); this._rootComponents.push(componentRef); // Get the listeners lazily to prevent DI cycles. @@ -463,12 +485,8 @@ export class ApplicationRef_ extends ApplicationRef { listeners.forEach((listener) => listener(componentRef)); } - /** @internal */ - _unloadComponent(componentRef: ComponentRef): void { - if (this._rootComponents.indexOf(componentRef) == -1) { - return; - } - this.unregisterChangeDetector(componentRef.changeDetectorRef); + private _unloadComponent(componentRef: ComponentRef): void { + this.detachView(componentRef.hostView); ListWrapper.remove(this._rootComponents, componentRef); } @@ -480,9 +498,9 @@ export class ApplicationRef_ extends ApplicationRef { const scope = ApplicationRef_._tickScope(); try { this._runningTick = true; - this._changeDetectorRefs.forEach((detector) => detector.detectChanges()); + this._views.forEach((view) => view.ref.detectChanges()); if (this._enforceNoNewChanges) { - this._changeDetectorRefs.forEach((detector) => detector.checkNoChanges()); + this._views.forEach((view) => view.ref.checkNoChanges()); } } finally { this._runningTick = false; @@ -492,9 +510,11 @@ export class ApplicationRef_ extends ApplicationRef { ngOnDestroy() { // TODO(alxhub): Dispose of the NgZone. - this._rootComponents.slice().forEach((component) => component.destroy()); + this._views.slice().forEach((view) => view.destroy()); } + get viewCount() { return this._views.length; } + get componentTypes(): Type[] { return this._rootComponentTypes; } get components(): ComponentRef[] { return this._rootComponents; } diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index d4ccc90b0b..5c8bb69ddb 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; import {ListWrapper} from '../facade/collection'; @@ -41,7 +42,10 @@ export abstract class AppView { lastRootNode: any; allNodes: any[]; disposables: Function[]; - viewContainer: ViewContainer = null; + viewContainer: ViewContainer; + // This will be set if a view is directly attached to an ApplicationRef + // and not to a view container. + appRef: ApplicationRef; numberOfChecks: number = 0; @@ -138,10 +142,12 @@ export abstract class AppView { injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); } detachAndDestroy() { - if (this._hasExternalHostElement) { - this.detach(); - } else if (isPresent(this.viewContainer)) { + if (this.viewContainer) { this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this)); + } else if (this.appRef) { + this.appRef.detachView(this.ref); + } else if (this._hasExternalHostElement) { + this.detach(); } this.destroy(); } @@ -196,6 +202,7 @@ export abstract class AppView { projectedViews.splice(index, 1); } } + this.appRef = null; this.viewContainer = null; this.dirtyParentQueriesInternal(); } @@ -208,7 +215,18 @@ export abstract class AppView { } } + attachToAppRef(appRef: ApplicationRef) { + if (this.viewContainer) { + throw new Error('This view is already attached to a ViewContainer!'); + } + this.appRef = appRef; + this.dirtyParentQueriesInternal(); + } + attachAfter(viewContainer: ViewContainer, prevView: AppView) { + if (this.appRef) { + throw new Error('This view is already attached directly to the ApplicationRef!'); + } this._renderAttach(viewContainer, prevView); this.viewContainer = viewContainer; if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) { diff --git a/modules/@angular/core/test/application_ref_spec.ts b/modules/@angular/core/test/application_ref_spec.ts index b80f7c2220..61895f9eb0 100644 --- a/modules/@angular/core/test/application_ref_spec.ts +++ b/modules/@angular/core/test/application_ref_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref'; import {ErrorHandler} from '@angular/core/src/error_handler'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; @@ -16,9 +16,7 @@ import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {expect} from '@angular/platform-browser/testing/matchers'; import {ServerModule} from '@angular/platform-server'; -import {TestBed, async, inject, withModule} from '../testing'; - -import {SpyChangeDetectorRef} from './spies'; +import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing'; @Component({selector: 'comp', template: 'hello'}) class SomeComponent { @@ -74,13 +72,16 @@ export function main() { beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => { - const cdRef = new SpyChangeDetectorRef(); + const view = jasmine.createSpyObj('view', ['detach', 'attachToAppRef']); + const viewRef = jasmine.createSpyObj('viewRef', ['detectChanges']); + viewRef.internalView = view; + view.ref = viewRef; try { - ref.registerChangeDetector(cdRef); - cdRef.spy('detectChanges').and.callFake(() => ref.tick()); + ref.attachView(viewRef); + viewRef.detectChanges.and.callFake(() => ref.tick()); expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); } finally { - ref.unregisterChangeDetector(cdRef); + ref.detachView(viewRef); } })); @@ -261,6 +262,84 @@ export function main() { }); })); }); + + describe('attachView / detachView', () => { + @Component({template: '{{name}}'}) + class MyComp { + name = 'Initial'; + } + + @Component({template: ''}) + class ContainerComp { + @ViewChild('vc', {read: ViewContainerRef}) + vc: ViewContainerRef; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MyComp, ContainerComp], + providers: [{provide: ComponentFixtureNoNgZone, useValue: true}] + }); + }); + + it('should dirty check attached views', () => { + const comp = TestBed.createComponent(MyComp); + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + expect(appRef.viewCount).toBe(0); + + appRef.tick(); + expect(comp.nativeElement).toHaveText(''); + + appRef.attachView(comp.componentRef.hostView); + appRef.tick(); + expect(appRef.viewCount).toBe(1); + expect(comp.nativeElement).toHaveText('Initial'); + }); + + it('should not dirty check detached views', () => { + const comp = TestBed.createComponent(MyComp); + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + + appRef.attachView(comp.componentRef.hostView); + appRef.tick(); + expect(comp.nativeElement).toHaveText('Initial'); + + appRef.detachView(comp.componentRef.hostView); + comp.componentInstance.name = 'New'; + appRef.tick(); + expect(appRef.viewCount).toBe(0); + expect(comp.nativeElement).toHaveText('Initial'); + }); + + it('should detach attached views if they are destroyed', () => { + const comp = TestBed.createComponent(MyComp); + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + + appRef.attachView(comp.componentRef.hostView); + comp.destroy(); + + expect(appRef.viewCount).toBe(0); + }); + + it('should not allow to attach a view to both, a view container and the ApplicationRef', + () => { + const comp = TestBed.createComponent(MyComp); + const hostView = comp.componentRef.hostView; + const containerComp = TestBed.createComponent(ContainerComp); + containerComp.detectChanges(); + const vc = containerComp.componentInstance.vc; + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + + vc.insert(hostView); + expect(() => appRef.attachView(hostView)) + .toThrowError('This view is already attached to a ViewContainer!'); + vc.detach(0); + + appRef.attachView(hostView); + expect(() => vc.insert(hostView)) + .toThrowError('This view is already attached directly to the ApplicationRef!'); + }); + }); }); } diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 3c3f95b1b2..9c98bcd23b 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -150,7 +150,10 @@ export declare class ApplicationModule { export declare abstract class ApplicationRef { componentTypes: Type[]; components: ComponentRef[]; + viewCount: any; + attachView(view: ViewRef): void; abstract bootstrap(componentFactory: ComponentFactory | Type): ComponentRef; + detachView(view: ViewRef): void; abstract tick(): void; }