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 {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; }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue