diff --git a/modules/angular2/src/core/application.ts b/modules/angular2/src/core/application.ts index a19481ada7..4e7e283b01 100644 --- a/modules/angular2/src/core/application.ts +++ b/modules/angular2/src/core/application.ts @@ -50,6 +50,7 @@ import {TestabilityRegistry, Testability} from 'angular2/src/core/testability/te import {AppViewPool, APP_VIEW_POOL_CAPACITY} from 'angular2/src/core/compiler/view_pool'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils'; +import {AppViewListener} from 'angular2/src/core/compiler/view_listener'; import {ProtoViewFactory} from 'angular2/src/core/compiler/proto_view_factory'; import {Renderer, RenderCompiler} from 'angular2/src/render/api'; import {DomRenderer, DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer'; @@ -112,6 +113,7 @@ function _injectorBindings(appComponentType): List> { bind(APP_VIEW_POOL_CAPACITY).toValue(10000), AppViewManager, AppViewManagerUtils, + AppViewListener, Compiler, CompilerCache, TemplateResolver, diff --git a/modules/angular2/src/core/compiler/view_listener.ts b/modules/angular2/src/core/compiler/view_listener.ts new file mode 100644 index 0000000000..360960203c --- /dev/null +++ b/modules/angular2/src/core/compiler/view_listener.ts @@ -0,0 +1,11 @@ +import {Injectable} from 'angular2/di'; +import * as viewModule from './view'; + +/** + * Listener for view creation / destruction. + */ +@Injectable() +export class AppViewListener { + viewCreated(view: viewModule.AppView) {} + viewDestroyed(view: viewModule.AppView) {} +} diff --git a/modules/angular2/src/core/compiler/view_manager.ts b/modules/angular2/src/core/compiler/view_manager.ts index f571d4825a..254613f942 100644 --- a/modules/angular2/src/core/compiler/view_manager.ts +++ b/modules/angular2/src/core/compiler/view_manager.ts @@ -7,6 +7,7 @@ import {ViewContainerRef} from './view_container_ref'; import {Renderer, RenderViewRef} from 'angular2/src/render/api'; import {AppViewManagerUtils} from './view_manager_utils'; import {AppViewPool} from './view_pool'; +import {AppViewListener} from './view_listener'; /** * Entry point for creating, moving views in the view hierarchy and destroying views. @@ -16,13 +17,16 @@ import {AppViewPool} from './view_pool'; @Injectable() export class AppViewManager { _viewPool: AppViewPool; + _viewListener: AppViewListener; _utils: AppViewManagerUtils; _renderer: Renderer; - constructor(viewPool: AppViewPool, utils: AppViewManagerUtils, renderer: Renderer) { - this._renderer = renderer; + constructor(viewPool: AppViewPool, viewListener: AppViewListener, utils: AppViewManagerUtils, + renderer: Renderer) { this._viewPool = viewPool; + this._viewListener = viewListener; this._utils = utils; + this._renderer = renderer; } getComponentView(hostLocation: ElementRef): ViewRef { @@ -74,6 +78,7 @@ export class AppViewManager { var hostView = this._utils.createView(hostProtoView, renderView, this, this._renderer); this._renderer.setEventDispatcher(hostView.render, hostView); this._createViewRecurse(hostView); + this._viewListener.viewCreated(hostView); this._utils.hydrateRootHostView(hostView, injector); this._viewHydrateRecurse(hostView); @@ -88,6 +93,7 @@ export class AppViewManager { // We do want to destroy the component view though. this._viewDehydrateRecurse(hostView, true); this._renderer.destroyView(hostView.render); + this._viewListener.viewDestroyed(hostView); } createFreeHostView(parentComponentLocation: ElementRef, hostProtoViewRef: ProtoViewRef, @@ -175,6 +181,7 @@ export class AppViewManager { this._renderer); this._renderer.setEventDispatcher(view.render, view); this._createViewRecurse(view); + this._viewListener.viewCreated(view); } return view; } @@ -192,8 +199,11 @@ export class AppViewManager { } _destroyPooledView(view: viewModule.AppView) { - // TODO: if the pool is full, call renderer.destroyView as well! - this._viewPool.returnView(view); + var wasReturned = this._viewPool.returnView(view); + if (!wasReturned) { + this._renderer.destroyView(view.render); + this._viewListener.viewDestroyed(view); + } } _destroyViewInContainer(parentView, boundElementIndex, atIndex: number) { diff --git a/modules/angular2/src/core/compiler/view_pool.ts b/modules/angular2/src/core/compiler/view_pool.ts index 94c384fa5d..12f277ca55 100644 --- a/modules/angular2/src/core/compiler/view_pool.ts +++ b/modules/angular2/src/core/compiler/view_pool.ts @@ -27,15 +27,17 @@ export class AppViewPool { return null; } - returnView(view: viewModule.AppView) { + returnView(view: viewModule.AppView): boolean { var protoView = view.proto; var pooledViews = MapWrapper.get(this._pooledViewsPerProtoView, protoView); if (isBlank(pooledViews)) { pooledViews = []; MapWrapper.set(this._pooledViewsPerProtoView, protoView, pooledViews); } - if (pooledViews.length < this._poolCapacityPerProtoView) { + var haveRemainingCapacity = pooledViews.length < this._poolCapacityPerProtoView; + if (haveRemainingCapacity) { ListWrapper.push(pooledViews, view); } + return haveRemainingCapacity; } } diff --git a/modules/angular2/test/core/compiler/view_manager_spec.ts b/modules/angular2/test/core/compiler/view_manager_spec.ts index d3709b5815..957e6a48ab 100644 --- a/modules/angular2/test/core/compiler/view_manager_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_spec.ts @@ -29,6 +29,7 @@ import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; import {Component} from 'angular2/annotations'; import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils'; +import {AppViewListener} from 'angular2/src/core/compiler/view_listener'; import {AppViewPool} from 'angular2/src/core/compiler/view_pool'; export function main() { @@ -37,6 +38,7 @@ export function main() { describe('AppViewManager', () => { var renderer; var utils; + var viewListener; var viewPool; var manager; var directiveResolver; @@ -113,8 +115,9 @@ export function main() { directiveResolver = new DirectiveResolver(); renderer = new SpyRenderer(); utils = new SpyAppViewManagerUtils(); + viewListener = new SpyAppViewListener(); viewPool = new SpyAppViewPool(); - manager = new AppViewManager(viewPool, utils, renderer); + manager = new AppViewManager(viewPool, viewListener, utils, renderer); createdViews = []; createdRenderViews = []; @@ -149,6 +152,7 @@ export function main() { ListWrapper.push(createdRenderViews, rv); return rv; }); + viewPool.spy('returnView').andReturn(true); }); describe('createDynamicComponentView', () => { @@ -165,6 +169,7 @@ export function main() { elementRef(wrapView(hostView), 0), wrapPv(componentProtoView), null, null))) .toBe(createdViews[0]); expect(createdViews[0].proto).toBe(componentProtoView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); }); it('should get the view from the pool', () => { @@ -178,6 +183,7 @@ export function main() { .toBe(createdView); expect(utils.spy('createView')).not.toHaveBeenCalled(); expect(renderer.spy('createView')).not.toHaveBeenCalled(); + expect(viewListener.spy('viewCreated')).not.toHaveBeenCalled(); }); it('should attach the view', () => { @@ -315,6 +321,7 @@ export function main() { wrapPv(hostProtoView), null))) .toBe(createdViews[0]); expect(createdViews[0].proto).toBe(hostProtoView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); }); it('should attachAndHydrate the view', () => { @@ -377,7 +384,16 @@ export function main() { it('should return the view to the pool', () => { manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); expect(viewPool.spy('returnView')).toHaveBeenCalledWith(hostView); + expect(renderer.spy('destroyView')).not.toHaveBeenCalled(); }); + + it('should destroy the view if the pool is full', () => { + viewPool.spy('returnView').andReturn(false); + manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); + expect(renderer.spy('destroyView')).toHaveBeenCalledWith(hostView.render); + expect(viewListener.spy('viewDestroyed')).toHaveBeenCalledWith(hostView); + }); + }); describe('recursively destroy inPlaceHostViews', () => { @@ -395,6 +411,7 @@ export function main() { expect(internalView(manager.createRootHostView(wrapPv(hostProtoView), null, null))) .toBe(createdViews[0]); expect(createdViews[0].proto).toBe(hostProtoView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); }); it('should hydrate the view', () => { @@ -444,6 +461,7 @@ export function main() { it('should destroy the render view', () => { manager.destroyRootHostView(wrapView(hostView)); expect(renderer.spy('destroyView')).toHaveBeenCalledWith(hostRenderViewRef); + expect(viewListener.spy('viewDestroyed')).toHaveBeenCalledWith(hostView); }); it('should not return the view to the pool', () => { @@ -473,6 +491,7 @@ export function main() { wrapPv(childProtoView), null))) .toBe(createdViews[0]); expect(createdViews[0].proto).toBe(childProtoView); + expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); }); it('should attach the view', () => { @@ -622,6 +641,13 @@ class SpyAppViewManagerUtils extends SpyObject { noSuchMethod(m) { return super.noSuchMethod(m) } } +@proxy +@IMPLEMENTS(AppViewListener) +class SpyAppViewListener extends SpyObject { + constructor() { super(AppViewListener); } + noSuchMethod(m) { return super.noSuchMethod(m) } +} + @proxy @IMPLEMENTS(ElementInjector) class SpyElementInjector extends SpyObject { diff --git a/modules/angular2/test/core/compiler/view_pool_spec.ts b/modules/angular2/test/core/compiler/view_pool_spec.ts index 78f9dd1627..213961dd9a 100644 --- a/modules/angular2/test/core/compiler/view_pool_spec.ts +++ b/modules/angular2/test/core/compiler/view_pool_spec.ts @@ -58,9 +58,9 @@ export function main() { var view1 = createView(pv); var view2 = createView(pv); var view3 = createView(pv); - vf.returnView(view1); - vf.returnView(view2); - vf.returnView(view3); + expect(vf.returnView(view1)).toBe(true); + expect(vf.returnView(view2)).toBe(true); + expect(vf.returnView(view3)).toBe(false); expect(vf.getView(pv)).toBe(view2); expect(vf.getView(pv)).toBe(view1);