feat(view): introduce free embedded views
Free embedded views are view instances that are created logically in the same was as views of a ViewContainer, but their dom nodes are not attached. BREAKING CHANGE: - `Renderer.detachFreeHostView` was renamed to `Renderer.detachFreeView` - `DomRenderer.getHostElement()` was generalized into `DomRenderer.getRootNodes()`
This commit is contained in:
		
							parent
							
								
									9ce0870f6c
								
							
						
					
					
						commit
						5030ffb01c
					
				| @ -23,12 +23,9 @@ import * as renderApi from 'angular2/src/render/api'; | ||||
| import {EventDispatcher} from 'angular2/src/render/api'; | ||||
| 
 | ||||
| export class AppViewContainer { | ||||
|   views: List<AppView>; | ||||
| 
 | ||||
|   constructor() { | ||||
|     // The order in this list matches the DOM order.
 | ||||
|     this.views = []; | ||||
|   } | ||||
|   // The order in this list matches the DOM order.
 | ||||
|   views: List<AppView> = []; | ||||
|   freeViews: List<AppView> = []; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -115,6 +115,24 @@ export class AppViewManager { | ||||
|     this._destroyFreeHostView(parentView, hostView); | ||||
|   } | ||||
| 
 | ||||
|   createFreeEmbeddedView(location: ElementRef, protoViewRef: ProtoViewRef, | ||||
|                          injector: Injector = null): ViewRef { | ||||
|     var protoView = internalProtoView(protoViewRef); | ||||
|     var parentView = internalView(location.parentView); | ||||
|     var boundElementIndex = location.boundElementIndex; | ||||
| 
 | ||||
|     var view = this._createPooledView(protoView); | ||||
|     this._utils.attachAndHydrateFreeEmbeddedView(parentView, boundElementIndex, view, injector); | ||||
|     this._viewHydrateRecurse(view); | ||||
|     return new ViewRef(view); | ||||
|   } | ||||
| 
 | ||||
|   destroyFreeEmbeddedView(location: ElementRef, viewRef: ViewRef) { | ||||
|     var parentView = internalView(location.parentView); | ||||
|     var boundElementIndex = location.boundElementIndex; | ||||
|     this._destroyFreeEmbeddedView(parentView, boundElementIndex, internalView(viewRef)); | ||||
|   } | ||||
| 
 | ||||
|   createViewInContainer(viewContainerLocation: ElementRef, atIndex: number, | ||||
|                         protoViewRef: ProtoViewRef, context: ElementRef = null, | ||||
|                         injector: Injector = null): ViewRef { | ||||
| @ -225,11 +243,18 @@ export class AppViewManager { | ||||
| 
 | ||||
|   _destroyFreeHostView(parentView, hostView) { | ||||
|     this._viewDehydrateRecurse(hostView, true); | ||||
|     this._renderer.detachFreeHostView(parentView.render, hostView.render); | ||||
|     this._renderer.detachFreeView(hostView.render); | ||||
|     this._utils.detachFreeHostView(parentView, hostView); | ||||
|     this._destroyPooledView(hostView); | ||||
|   } | ||||
| 
 | ||||
|   _destroyFreeEmbeddedView(parentView, boundElementIndex, view) { | ||||
|     this._viewDehydrateRecurse(view, false); | ||||
|     this._renderer.detachFreeView(view.render); | ||||
|     this._utils.detachFreeEmbeddedView(parentView, boundElementIndex, view); | ||||
|     this._destroyPooledView(view); | ||||
|   } | ||||
| 
 | ||||
|   _viewHydrateRecurse(view: viewModule.AppView) { | ||||
|     this._renderer.hydrateView(view.render); | ||||
| 
 | ||||
| @ -260,6 +285,9 @@ export class AppViewManager { | ||||
|         for (var j = vc.views.length - 1; j >= 0; j--) { | ||||
|           this._destroyViewInContainer(view, i, j); | ||||
|         } | ||||
|         for (var j = vc.freeViews.length - 1; j >= 0; j--) { | ||||
|           this._destroyFreeEmbeddedView(view, i, j); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -110,6 +110,28 @@ export class AppViewManagerUtils { | ||||
|     ListWrapper.remove(parentView.freeHostViews, hostView); | ||||
|   } | ||||
| 
 | ||||
|   attachAndHydrateFreeEmbeddedView(parentView: viewModule.AppView, boundElementIndex: number, | ||||
|                                    view: viewModule.AppView, injector: Injector = null) { | ||||
|     parentView.changeDetector.addChild(view.changeDetector); | ||||
|     var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex); | ||||
|     ListWrapper.push(viewContainer.freeViews, view); | ||||
|     var elementInjector = parentView.elementInjectors[boundElementIndex]; | ||||
|     for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) { | ||||
|       view.rootElementInjectors[i].link(elementInjector); | ||||
|     } | ||||
|     this._hydrateView(view, injector, elementInjector, parentView.context, parentView.locals); | ||||
|   } | ||||
| 
 | ||||
|   detachFreeEmbeddedView(parentView: viewModule.AppView, boundElementIndex: number, | ||||
|                          view: viewModule.AppView) { | ||||
|     var viewContainer = parentView.viewContainers[boundElementIndex]; | ||||
|     view.changeDetector.remove(); | ||||
|     ListWrapper.remove(viewContainer.freeViews, view); | ||||
|     for (var i = 0; i < view.rootElementInjectors.length; ++i) { | ||||
|       view.rootElementInjectors[i].unlink(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   attachViewInContainer(parentView: viewModule.AppView, boundElementIndex: number, | ||||
|                         contextView: viewModule.AppView, contextBoundElementIndex: number, | ||||
|                         atIndex: number, view: viewModule.AppView) { | ||||
| @ -118,11 +140,7 @@ export class AppViewManagerUtils { | ||||
|       contextBoundElementIndex = boundElementIndex; | ||||
|     } | ||||
|     parentView.changeDetector.addChild(view.changeDetector); | ||||
|     var viewContainer = parentView.viewContainers[boundElementIndex]; | ||||
|     if (isBlank(viewContainer)) { | ||||
|       viewContainer = new viewModule.AppViewContainer(); | ||||
|       parentView.viewContainers[boundElementIndex] = viewContainer; | ||||
|     } | ||||
|     var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex); | ||||
|     ListWrapper.insert(viewContainer.views, atIndex, view); | ||||
|     var sibling; | ||||
|     if (atIndex == 0) { | ||||
| @ -208,6 +226,15 @@ export class AppViewManagerUtils { | ||||
|     view.changeDetector.hydrate(view.context, view.locals, view); | ||||
|   } | ||||
| 
 | ||||
|   _getOrCreateViewContainer(parentView: viewModule.AppView, boundElementIndex: number) { | ||||
|     var viewContainer = parentView.viewContainers[boundElementIndex]; | ||||
|     if (isBlank(viewContainer)) { | ||||
|       viewContainer = new viewModule.AppViewContainer(); | ||||
|       parentView.viewContainers[boundElementIndex] = viewContainer; | ||||
|     } | ||||
|     return viewContainer; | ||||
|   } | ||||
| 
 | ||||
|   _setUpEventEmitters(view: viewModule.AppView, elementInjector: eli.ElementInjector, | ||||
|                       boundElementIndex: number) { | ||||
|     var emitters = elementInjector.getEventEmitterAccessors(); | ||||
|  | ||||
| @ -237,9 +237,9 @@ export class Renderer { | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Detaches a free host view's element from the DOM. | ||||
|    * Detaches a free view's element from the DOM. | ||||
|    */ | ||||
|   detachFreeHostView(parentHostViewRef: RenderViewRef, hostViewRef: RenderViewRef) {} | ||||
|   detachFreeView(view: RenderViewRef) {} | ||||
| 
 | ||||
|   /** | ||||
|    * Creates a regular view out of the given ProtoView | ||||
|  | ||||
| @ -47,9 +47,9 @@ export class DomRenderer extends Renderer { | ||||
|     return new DomViewRef(this._createView(hostProtoView, element)); | ||||
|   } | ||||
| 
 | ||||
|   detachFreeHostView(parentHostViewRef: RenderViewRef, hostViewRef: RenderViewRef) { | ||||
|     var hostView = resolveInternalDomView(hostViewRef); | ||||
|     this._removeViewNodes(hostView); | ||||
|   detachFreeView(viewRef: RenderViewRef) { | ||||
|     var view = resolveInternalDomView(viewRef); | ||||
|     this._removeViewNodes(view); | ||||
|   } | ||||
| 
 | ||||
|   createView(protoViewRef: RenderProtoViewRef): RenderViewRef { | ||||
| @ -83,9 +83,8 @@ export class DomRenderer extends Renderer { | ||||
|     this._moveViewNodesIntoParent(componentView.shadowRoot, componentView); | ||||
|   } | ||||
| 
 | ||||
|   getHostElement(hostViewRef: RenderViewRef) { | ||||
|     var hostView = resolveInternalDomView(hostViewRef); | ||||
|     return hostView.boundElements[0]; | ||||
|   getRootNodes(viewRef: RenderViewRef): List</*node*/ any> { | ||||
|     return resolveInternalDomView(viewRef).rootNodes; | ||||
|   } | ||||
| 
 | ||||
|   detachComponentView(hostViewRef: RenderViewRef, boundElementIndex: number, | ||||
|  | ||||
| @ -257,7 +257,7 @@ class ImperativeViewComponentUsingNgComponent { | ||||
|     renderer.setComponentViewRootNodes(shadowViewRef.render, [div]); | ||||
|     this.done = dynamicComponentLoader.loadIntoNewLocation(ChildComp, self, null) | ||||
|                     .then((componentRef) => { | ||||
|                       var element = renderer.getHostElement(componentRef.hostView.render); | ||||
|                       var element = renderer.getRootNodes(componentRef.hostView.render)[0]; | ||||
|                       DOM.appendChild(div, element); | ||||
|                       return componentRef; | ||||
|                     }); | ||||
|  | ||||
| @ -29,11 +29,12 @@ import { | ||||
|   isJsObject, | ||||
|   global, | ||||
|   stringify, | ||||
|   CONST | ||||
|   CONST, | ||||
|   CONST_EXPR | ||||
| } from 'angular2/src/facade/lang'; | ||||
| import {PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; | ||||
| 
 | ||||
| import {Injector, bind, Injectable, Binding, FORWARD_REF} from 'angular2/di'; | ||||
| import {Injector, bind, Injectable, Binding, FORWARD_REF, OpaqueToken, Inject} from 'angular2/di'; | ||||
| import { | ||||
|   PipeRegistry, | ||||
|   defaultPipeRegistry, | ||||
| @ -63,17 +64,21 @@ import {NgIf} from 'angular2/src/directives/ng_if'; | ||||
| import {NgFor} from 'angular2/src/directives/ng_for'; | ||||
| 
 | ||||
| import {ViewContainerRef} from 'angular2/src/core/compiler/view_container_ref'; | ||||
| import {ProtoViewRef} from 'angular2/src/core/compiler/view_ref'; | ||||
| import {ProtoViewRef, ViewRef} from 'angular2/src/core/compiler/view_ref'; | ||||
| import {Compiler} from 'angular2/src/core/compiler/compiler'; | ||||
| import {ElementRef} from 'angular2/src/core/compiler/element_ref'; | ||||
| 
 | ||||
| import {DomRenderer} from 'angular2/src/render/dom/dom_renderer'; | ||||
| import {AppViewManager} from 'angular2/src/core/compiler/view_manager'; | ||||
| 
 | ||||
| const ANCHOR_ELEMENT = CONST_EXPR(new OpaqueToken('AnchorElement')); | ||||
| 
 | ||||
| export function main() { | ||||
|   describe('integration tests', function() { | ||||
|     var ctx; | ||||
| 
 | ||||
|     beforeEachBindings(() => [bind(ANCHOR_ELEMENT).toValue(el('<div></div>'))]); | ||||
| 
 | ||||
|     beforeEach(() => { ctx = new MyComp(); }); | ||||
| 
 | ||||
| 
 | ||||
| @ -1124,6 +1129,28 @@ export function main() { | ||||
|          }); | ||||
|        })); | ||||
| 
 | ||||
|     it('should support free embedded views', | ||||
|        inject([TestBed, AsyncTestCompleter, ANCHOR_ELEMENT], (tb, async, anchorElement) => { | ||||
|          tb.overrideView(MyComp, new viewAnn.View({ | ||||
|            template: '<div><div *some-impvp="ctxBoolProp">hello</div></div>', | ||||
|            directives: [SomeImperativeViewport] | ||||
|          })); | ||||
|          tb.createView(MyComp).then((view) => { | ||||
|            view.detectChanges(); | ||||
|            expect(anchorElement).toHaveText(''); | ||||
| 
 | ||||
|            view.context.ctxBoolProp = true; | ||||
|            view.detectChanges(); | ||||
|            expect(anchorElement).toHaveText('hello'); | ||||
| 
 | ||||
|            view.context.ctxBoolProp = false; | ||||
|            view.detectChanges(); | ||||
|            expect(view.rootNodes).toHaveText(''); | ||||
| 
 | ||||
|            async.done(); | ||||
|          }); | ||||
|        })); | ||||
| 
 | ||||
|     // Disabled until a solution is found, refs:
 | ||||
|     // - https://github.com/angular/angular/issues/776
 | ||||
|     // - https://github.com/angular/angular/commit/81f3f32
 | ||||
| @ -1640,3 +1667,30 @@ class ChildConsumingEventBus { | ||||
| 
 | ||||
|   constructor(@Unbounded() bus: EventBus) { this.bus = bus; } | ||||
| } | ||||
| 
 | ||||
| @Directive({selector: '[some-impvp]', properties: ['someImpvp']}) | ||||
| @Injectable() | ||||
| class SomeImperativeViewport { | ||||
|   view: ViewRef; | ||||
|   anchor; | ||||
|   constructor(public element: ElementRef, public protoView: ProtoViewRef, | ||||
|               public viewManager: AppViewManager, public renderer: DomRenderer, | ||||
|               @Inject(ANCHOR_ELEMENT) anchor) { | ||||
|     this.view = null; | ||||
|     this.anchor = anchor; | ||||
|   } | ||||
| 
 | ||||
|   set someImpvp(value: boolean) { | ||||
|     if (isPresent(this.view)) { | ||||
|       this.viewManager.destroyFreeEmbeddedView(this.element, this.view); | ||||
|       this.view = null; | ||||
|     } | ||||
|     if (value) { | ||||
|       this.view = this.viewManager.createFreeEmbeddedView(this.element, this.protoView); | ||||
|       var nodes = this.renderer.getRootNodes(this.view.render); | ||||
|       for (var i = 0; i < nodes.length; i++) { | ||||
|         DOM.appendChild(this.anchor, nodes[i]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -377,8 +377,7 @@ export function main() { | ||||
| 
 | ||||
|         it('should detach the render view', () => { | ||||
|           manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView)); | ||||
|           expect(renderer.spy('detachFreeHostView')) | ||||
|               .toHaveBeenCalledWith(parentView.render, hostRenderViewRef); | ||||
|           expect(renderer.spy('detachFreeView')).toHaveBeenCalledWith(hostRenderViewRef); | ||||
|         }); | ||||
| 
 | ||||
|         it('should return the view to the pool', () => { | ||||
| @ -402,6 +401,101 @@ export function main() { | ||||
| 
 | ||||
|     }); | ||||
| 
 | ||||
|     describe('createFreeEmbeddedView', () => { | ||||
| 
 | ||||
|       // Note: We don't add tests for recursion or viewpool here as we assume that
 | ||||
|       // this is using the same mechanism as the other methods...
 | ||||
| 
 | ||||
|       describe('basic functionality', () => { | ||||
|         var parentView, childProtoView; | ||||
|         beforeEach(() => { | ||||
|           parentView = createView(createProtoView([createEmptyElBinder()])); | ||||
|           childProtoView = createProtoView(); | ||||
|         }); | ||||
| 
 | ||||
|         it('should create the view', () => { | ||||
|           expect(internalView(manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), | ||||
|                                                              wrapPv(childProtoView), null))) | ||||
|               .toBe(createdViews[0]); | ||||
|           expect(createdViews[0].proto).toBe(childProtoView); | ||||
|           expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]); | ||||
|         }); | ||||
| 
 | ||||
|         it('should attachAndHydrate the view', () => { | ||||
|           var injector = new Injector([], null, false); | ||||
|           manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), | ||||
|                                          wrapPv(childProtoView), injector); | ||||
|           expect(utils.spy('attachAndHydrateFreeEmbeddedView')) | ||||
|               .toHaveBeenCalledWith(parentView, 0, createdViews[0], injector); | ||||
|           expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render); | ||||
|         }); | ||||
| 
 | ||||
|         it('should create and set the render view', () => { | ||||
|           manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), | ||||
|                                          wrapPv(childProtoView), null); | ||||
|           expect(renderer.spy('createView')).toHaveBeenCalledWith(childProtoView.render); | ||||
|           expect(createdViews[0].render).toBe(createdRenderViews[0]); | ||||
|         }); | ||||
| 
 | ||||
|         it('should set the event dispatcher', () => { | ||||
|           manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0), | ||||
|                                          wrapPv(childProtoView), null); | ||||
|           var cmpView = createdViews[0]; | ||||
|           expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     describe('destroyFreeEmbeddedView', () => { | ||||
|       describe('basic functionality', () => { | ||||
|         var parentView, childProtoView, childView; | ||||
|         beforeEach(() => { | ||||
|           parentView = createView(createProtoView([createEmptyElBinder()])); | ||||
|           childProtoView = createProtoView(); | ||||
|           childView = internalView(manager.createFreeEmbeddedView( | ||||
|               elementRef(wrapView(parentView), 0), wrapPv(childProtoView), null)); | ||||
|         }); | ||||
| 
 | ||||
|         it('should detach', () => { | ||||
|           manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); | ||||
|           expect(utils.spy('detachFreeEmbeddedView')) | ||||
|               .toHaveBeenCalledWith(parentView, 0, childView); | ||||
|         }); | ||||
| 
 | ||||
|         it('should dehydrate', () => { | ||||
|           manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); | ||||
|           expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(childView); | ||||
|           expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render); | ||||
|         }); | ||||
| 
 | ||||
|         it('should detach the render view', () => { | ||||
|           manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); | ||||
|           expect(renderer.spy('detachFreeView')).toHaveBeenCalledWith(childView.render); | ||||
|         }); | ||||
| 
 | ||||
|         it('should return the view to the pool', () => { | ||||
|           manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); | ||||
|           expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView); | ||||
|           expect(renderer.spy('destroyView')).not.toHaveBeenCalled(); | ||||
|         }); | ||||
| 
 | ||||
|         it('should destroy the view if the pool is full', () => { | ||||
|           viewPool.spy('returnView').andReturn(false); | ||||
|           manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView)); | ||||
|           expect(renderer.spy('destroyView')).toHaveBeenCalledWith(childView.render); | ||||
|           expect(viewListener.spy('viewDestroyed')).toHaveBeenCalledWith(childView); | ||||
|         }); | ||||
| 
 | ||||
|       }); | ||||
| 
 | ||||
|       describe('recursively destroyFreeEmbeddedView', () => { | ||||
|                                                           // TODO
 | ||||
|                                                       }); | ||||
| 
 | ||||
|     }); | ||||
| 
 | ||||
|     describe('createRootHostView', () => { | ||||
| 
 | ||||
|       var hostProtoView; | ||||
|  | ||||
| @ -40,17 +40,16 @@ export function main() { | ||||
|              }); | ||||
|        })); | ||||
| 
 | ||||
|     it('should create and destroy free host views', | ||||
|     it('should create and destroy free views', | ||||
|        inject([AsyncTestCompleter, DomTestbed], (async, tb) => { | ||||
|          tb.compiler.compileHost(someComponent) | ||||
|              .then((hostProtoViewDto) => { | ||||
|                var view = new TestView(tb.renderer.createView(hostProtoViewDto.render)); | ||||
|                var hostElement = tb.renderer.getHostElement(view.viewRef); | ||||
|                var hostElement = tb.renderer.getRootNodes(view.viewRef)[0]; | ||||
|                DOM.appendChild(tb.rootEl, hostElement); | ||||
| 
 | ||||
|                tb.renderer.detachFreeHostView(null, view.viewRef); | ||||
|                tb.renderer.detachFreeView(view.viewRef); | ||||
|                expect(DOM.parentElement(hostElement)).toBeFalsy(); | ||||
| 
 | ||||
|                async.done(); | ||||
|              }); | ||||
|        })); | ||||
|  | ||||
| @ -66,7 +66,7 @@ export class MdDialog { | ||||
|           // TODO(jelbourn): Don't use direct DOM access. Need abstraction to create an element
 | ||||
|           // directly on the document body (also needed for web workers stuff).
 | ||||
|           // Create a DOM node to serve as a physical host element for the dialog.
 | ||||
|           var dialogElement = this.domRenderer.getHostElement(containerRef.hostView.render); | ||||
|           var dialogElement = this.domRenderer.getRootNodes(containerRef.hostView.render)[0]; | ||||
|           DOM.appendChild(DOM.query('body'), dialogElement); | ||||
| 
 | ||||
|           // TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed.
 | ||||
| @ -111,7 +111,7 @@ export class MdDialog { | ||||
|         .then((componentRef) => { | ||||
|           // TODO(tbosch): clean this up when we have custom renderers
 | ||||
|           // (https://github.com/angular/angular/issues/1807)
 | ||||
|           var backdropElement = this.domRenderer.getHostElement(componentRef.hostView.render); | ||||
|           var backdropElement = this.domRenderer.getRootNodes(componentRef.hostView.render)[0]; | ||||
|           DOM.addClass(backdropElement, 'md-backdrop'); | ||||
|           DOM.appendChild(DOM.query('body'), backdropElement); | ||||
|           return componentRef; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user