feat(core): add `attachView` / `detachView` to ApplicationRef

This feature is useful to allow components / embedded views
to be dirty checked if they are not placed in any `ViewContainer`.

Closes #9293
This commit is contained in:
Tobias Bosch 2016-11-04 11:58:06 -07:00 committed by Victor Berchet
parent 9de76ebfa5
commit 9f7d32a326
4 changed files with 149 additions and 29 deletions

View File

@ -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<any>[] { return <ComponentRef<any>[]>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<any>[] = [];
private _rootComponentTypes: Type<any>[] = [];
private _changeDetectorRefs: ChangeDetectorRef[] = [];
private _views: AppView<any>[] = [];
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_<any>).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_<any>).internalView;
ListWrapper.remove(this._views, view);
view.detach();
}
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>): ComponentRef<C> {
@ -451,9 +474,8 @@ export class ApplicationRef_ extends ApplicationRef {
return compRef;
}
/** @internal */
_loadComponent(componentRef: ComponentRef<any>): void {
this._changeDetectorRefs.push(componentRef.changeDetectorRef);
private _loadComponent(componentRef: ComponentRef<any>): 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<any>): void {
if (this._rootComponents.indexOf(componentRef) == -1) {
return;
}
this.unregisterChangeDetector(componentRef.changeDetectorRef);
private _unloadComponent(componentRef: ComponentRef<any>): 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<any>[] { return this._rootComponentTypes; }
get components(): ComponentRef<any>[] { return this._rootComponents; }

View File

@ -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<T> {
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<T> {
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<T> {
projectedViews.splice(index, 1);
}
}
this.appRef = null;
this.viewContainer = null;
this.dirtyParentQueriesInternal();
}
@ -208,7 +215,18 @@ export abstract class AppView<T> {
}
}
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<any>) {
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) {

View File

@ -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 = <any>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: '<ng-container #vc></ng-container>'})
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!');
});
});
});
}

View File

@ -150,7 +150,10 @@ export declare class ApplicationModule {
export declare abstract class ApplicationRef {
componentTypes: Type<any>[];
components: ComponentRef<any>[];
viewCount: any;
attachView(view: ViewRef): void;
abstract bootstrap<C>(componentFactory: ComponentFactory<C> | Type<C>): ComponentRef<C>;
detachView(view: ViewRef): void;
abstract tick(): void;
}