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:
parent
9de76ebfa5
commit
9f7d32a326
|
@ -21,6 +21,8 @@ import {CompilerFactory, CompilerOptions} from './linker/compiler';
|
||||||
import {ComponentFactory, ComponentRef} from './linker/component_factory';
|
import {ComponentFactory, ComponentRef} from './linker/component_factory';
|
||||||
import {ComponentFactoryResolver} from './linker/component_factory_resolver';
|
import {ComponentFactoryResolver} from './linker/component_factory_resolver';
|
||||||
import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory';
|
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 {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
|
||||||
import {Testability, TestabilityRegistry} from './testability/testability';
|
import {Testability, TestabilityRegistry} from './testability/testability';
|
||||||
import {Type} from './type';
|
import {Type} from './type';
|
||||||
|
@ -387,6 +389,23 @@ export abstract class ApplicationRef {
|
||||||
* Get a list of components registered to this application.
|
* Get a list of components registered to this application.
|
||||||
*/
|
*/
|
||||||
get components(): ComponentRef<any>[] { return <ComponentRef<any>[]>unimplemented(); };
|
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()
|
@Injectable()
|
||||||
|
@ -397,7 +416,7 @@ export class ApplicationRef_ extends ApplicationRef {
|
||||||
private _bootstrapListeners: Function[] = [];
|
private _bootstrapListeners: Function[] = [];
|
||||||
private _rootComponents: ComponentRef<any>[] = [];
|
private _rootComponents: ComponentRef<any>[] = [];
|
||||||
private _rootComponentTypes: Type<any>[] = [];
|
private _rootComponentTypes: Type<any>[] = [];
|
||||||
private _changeDetectorRefs: ChangeDetectorRef[] = [];
|
private _views: AppView<any>[] = [];
|
||||||
private _runningTick: boolean = false;
|
private _runningTick: boolean = false;
|
||||||
private _enforceNoNewChanges: boolean = false;
|
private _enforceNoNewChanges: boolean = false;
|
||||||
|
|
||||||
|
@ -415,12 +434,16 @@ export class ApplicationRef_ extends ApplicationRef {
|
||||||
{next: () => { this._zone.run(() => { this.tick(); }); }});
|
{next: () => { this._zone.run(() => { this.tick(); }); }});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerChangeDetector(changeDetector: ChangeDetectorRef): void {
|
attachView(viewRef: ViewRef): void {
|
||||||
this._changeDetectorRefs.push(changeDetector);
|
const view = (viewRef as ViewRef_<any>).internalView;
|
||||||
|
this._views.push(view);
|
||||||
|
view.attachToAppRef(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterChangeDetector(changeDetector: ChangeDetectorRef): void {
|
detachView(viewRef: ViewRef): void {
|
||||||
ListWrapper.remove(this._changeDetectorRefs, changeDetector);
|
const view = (viewRef as ViewRef_<any>).internalView;
|
||||||
|
ListWrapper.remove(this._views, view);
|
||||||
|
view.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>): ComponentRef<C> {
|
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>): ComponentRef<C> {
|
||||||
|
@ -451,9 +474,8 @@ export class ApplicationRef_ extends ApplicationRef {
|
||||||
return compRef;
|
return compRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
private _loadComponent(componentRef: ComponentRef<any>): void {
|
||||||
_loadComponent(componentRef: ComponentRef<any>): void {
|
this.attachView(componentRef.hostView);
|
||||||
this._changeDetectorRefs.push(componentRef.changeDetectorRef);
|
|
||||||
this.tick();
|
this.tick();
|
||||||
this._rootComponents.push(componentRef);
|
this._rootComponents.push(componentRef);
|
||||||
// Get the listeners lazily to prevent DI cycles.
|
// Get the listeners lazily to prevent DI cycles.
|
||||||
|
@ -463,12 +485,8 @@ export class ApplicationRef_ extends ApplicationRef {
|
||||||
listeners.forEach((listener) => listener(componentRef));
|
listeners.forEach((listener) => listener(componentRef));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
private _unloadComponent(componentRef: ComponentRef<any>): void {
|
||||||
_unloadComponent(componentRef: ComponentRef<any>): void {
|
this.detachView(componentRef.hostView);
|
||||||
if (this._rootComponents.indexOf(componentRef) == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.unregisterChangeDetector(componentRef.changeDetectorRef);
|
|
||||||
ListWrapper.remove(this._rootComponents, componentRef);
|
ListWrapper.remove(this._rootComponents, componentRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,9 +498,9 @@ export class ApplicationRef_ extends ApplicationRef {
|
||||||
const scope = ApplicationRef_._tickScope();
|
const scope = ApplicationRef_._tickScope();
|
||||||
try {
|
try {
|
||||||
this._runningTick = true;
|
this._runningTick = true;
|
||||||
this._changeDetectorRefs.forEach((detector) => detector.detectChanges());
|
this._views.forEach((view) => view.ref.detectChanges());
|
||||||
if (this._enforceNoNewChanges) {
|
if (this._enforceNoNewChanges) {
|
||||||
this._changeDetectorRefs.forEach((detector) => detector.checkNoChanges());
|
this._views.forEach((view) => view.ref.checkNoChanges());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this._runningTick = false;
|
this._runningTick = false;
|
||||||
|
@ -492,9 +510,11 @@ export class ApplicationRef_ extends ApplicationRef {
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// TODO(alxhub): Dispose of the NgZone.
|
// 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 componentTypes(): Type<any>[] { return this._rootComponentTypes; }
|
||||||
|
|
||||||
get components(): ComponentRef<any>[] { return this._rootComponents; }
|
get components(): ComponentRef<any>[] { return this._rootComponents; }
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ApplicationRef} from '../application_ref';
|
||||||
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
|
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
|
||||||
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
|
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
|
||||||
import {ListWrapper} from '../facade/collection';
|
import {ListWrapper} from '../facade/collection';
|
||||||
|
@ -41,7 +42,10 @@ export abstract class AppView<T> {
|
||||||
lastRootNode: any;
|
lastRootNode: any;
|
||||||
allNodes: any[];
|
allNodes: any[];
|
||||||
disposables: Function[];
|
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;
|
numberOfChecks: number = 0;
|
||||||
|
|
||||||
|
@ -138,10 +142,12 @@ export abstract class AppView<T> {
|
||||||
injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); }
|
injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); }
|
||||||
|
|
||||||
detachAndDestroy() {
|
detachAndDestroy() {
|
||||||
if (this._hasExternalHostElement) {
|
if (this.viewContainer) {
|
||||||
this.detach();
|
|
||||||
} else if (isPresent(this.viewContainer)) {
|
|
||||||
this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this));
|
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();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
@ -196,6 +202,7 @@ export abstract class AppView<T> {
|
||||||
projectedViews.splice(index, 1);
|
projectedViews.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.appRef = null;
|
||||||
this.viewContainer = null;
|
this.viewContainer = null;
|
||||||
this.dirtyParentQueriesInternal();
|
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>) {
|
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._renderAttach(viewContainer, prevView);
|
||||||
this.viewContainer = viewContainer;
|
this.viewContainer = viewContainer;
|
||||||
if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) {
|
if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
|
||||||
import {ErrorHandler} from '@angular/core/src/error_handler';
|
import {ErrorHandler} from '@angular/core/src/error_handler';
|
||||||
import {ComponentRef} from '@angular/core/src/linker/component_factory';
|
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 {expect} from '@angular/platform-browser/testing/matchers';
|
||||||
import {ServerModule} from '@angular/platform-server';
|
import {ServerModule} from '@angular/platform-server';
|
||||||
|
|
||||||
import {TestBed, async, inject, withModule} from '../testing';
|
import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
|
||||||
|
|
||||||
import {SpyChangeDetectorRef} from './spies';
|
|
||||||
|
|
||||||
@Component({selector: 'comp', template: 'hello'})
|
@Component({selector: 'comp', template: 'hello'})
|
||||||
class SomeComponent {
|
class SomeComponent {
|
||||||
|
@ -74,13 +72,16 @@ export function main() {
|
||||||
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
|
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
|
||||||
|
|
||||||
it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => {
|
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 {
|
try {
|
||||||
ref.registerChangeDetector(cdRef);
|
ref.attachView(viewRef);
|
||||||
cdRef.spy('detectChanges').and.callFake(() => ref.tick());
|
viewRef.detectChanges.and.callFake(() => ref.tick());
|
||||||
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively');
|
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively');
|
||||||
} finally {
|
} 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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,10 @@ export declare class ApplicationModule {
|
||||||
export declare abstract class ApplicationRef {
|
export declare abstract class ApplicationRef {
|
||||||
componentTypes: Type<any>[];
|
componentTypes: Type<any>[];
|
||||||
components: ComponentRef<any>[];
|
components: ComponentRef<any>[];
|
||||||
|
viewCount: any;
|
||||||
|
attachView(view: ViewRef): void;
|
||||||
abstract bootstrap<C>(componentFactory: ComponentFactory<C> | Type<C>): ComponentRef<C>;
|
abstract bootstrap<C>(componentFactory: ComponentFactory<C> | Type<C>): ComponentRef<C>;
|
||||||
|
detachView(view: ViewRef): void;
|
||||||
abstract tick(): void;
|
abstract tick(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue