From ada1e642c57f19ad5cecde57db1aca1727bf2325 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Mon, 20 Apr 2015 11:34:53 -0700 Subject: [PATCH] feat(view): add imperative views --- modules/angular2/angular2.js | 2 + modules/angular2/src/core/annotations/view.js | 13 ++- modules/angular2/src/core/application.js | 7 +- .../angular2/src/core/compiler/compiler.js | 23 +++-- .../core/compiler/dynamic_component_loader.js | 24 +++-- .../src/core/compiler/element_injector.js | 22 ++--- modules/angular2/src/core/compiler/view.js | 10 +- .../src/core/compiler/view_factory.js | 7 +- .../src/core/compiler/view_hydrator.js | 50 +++++++--- modules/angular2/src/dom/browser_adapter.dart | 2 +- modules/angular2/src/render/api.js | 8 ++ .../src/render/dom/direct_dom_renderer.js | 22 +++++ .../src/render/dom/view/proto_view.js | 14 ++- .../src/render/dom/view/proto_view_builder.js | 11 ++- modules/angular2/src/render/dom/view/view.js | 6 +- .../src/render/dom/view/view_factory.js | 14 ++- .../src/render/dom/view/view_hydrator.js | 25 ++++- .../angular2/src/test_lib/test_injector.js | 1 + .../test/core/compiler/compiler_spec.js | 68 +++++++++----- .../compiler/dynamic_component_loader_spec.js | 63 +++++++++++++ .../core/compiler/element_injector_spec.js | 2 +- .../test/core/compiler/integration_spec.js | 34 ++++++- .../test/core/compiler/view_factory_spec.js | 2 +- .../test/core/compiler/view_hydrator_spec.js | 41 ++++++-- .../angular2/test/core/compiler/view_spec.js | 2 +- .../direct_dom_renderer_integration_spec.js | 10 ++ .../test/render/dom/integration_testbed.js | 2 +- .../render/dom/view/view_hydrator_spec.js | 93 ++++++++++++++++++- 28 files changed, 458 insertions(+), 120 deletions(-) diff --git a/modules/angular2/angular2.js b/modules/angular2/angular2.js index 218f94df14..2d5354143a 100644 --- a/modules/angular2/angular2.js +++ b/modules/angular2/angular2.js @@ -4,3 +4,5 @@ export * from './annotations'; export * from './directives'; export * from './forms'; export {Observable, EventEmitter} from 'angular2/src/facade/async'; +export * from 'angular2/src/render/api'; +export {DirectDomRenderer} from 'angular2/src/render/dom/direct_dom_renderer'; diff --git a/modules/angular2/src/core/annotations/view.js b/modules/angular2/src/core/annotations/view.js index 74e313c538..2ceecabc60 100644 --- a/modules/angular2/src/core/annotations/view.js +++ b/modules/angular2/src/core/annotations/view.js @@ -71,19 +71,28 @@ export class View { */ directives:any; //List; + /** + * Specify a custom renderer for this View. + * If this is set, neither `template`, `templateURL` nor `directives` are used. + */ + renderer:any; // string; + @CONST() constructor({ templateUrl, template, - directives + directives, + renderer }: { templateUrl: string, template: string, - directives: List + directives: List, + renderer: string }) { this.templateUrl = templateUrl; this.template = template; this.directives = directives; + this.renderer = renderer; } } diff --git a/modules/angular2/src/core/application.js b/modules/angular2/src/core/application.js index ab25fee1e6..f97a574685 100644 --- a/modules/angular2/src/core/application.js +++ b/modules/angular2/src/core/application.js @@ -75,7 +75,7 @@ function _injectorBindings(appComponentType): List { // We need to do this here to ensure that we create Testability and // it's ready on the window for users. registry.registerApplication(appElement, testability); - return dynamicComponentLoader.loadIntoNewLocation(appElement, appComponentAnnotatedType.type, injector); + return dynamicComponentLoader.loadIntoNewLocation(appComponentAnnotatedType.type, null, appElement, injector); }, [DynamicComponentLoader, Injector, appElementToken, appComponentAnnotatedTypeToken, Testability, TestabilityRegistry]), @@ -91,6 +91,7 @@ function _injectorBindings(appComponentType): List { bind(ShadowDomStrategy).toFactory( (styleUrlResolver, doc) => new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, doc.head), [StyleUrlResolver, appDocumentToken]), + DirectDomRenderer, bind(Renderer).toClass(DirectDomRenderer), bind(rc.Compiler).toClass(rc.DefaultCompiler), // TODO(tbosch): We need an explicit factory here, as @@ -105,8 +106,8 @@ function _injectorBindings(appComponentType): List { // TODO(tbosch): We need an explicit factory here, as // we are getting errors in dart2js with mirrors... bind(ViewFactory).toFactory( - (capacity, renderer, appViewHydrator) => new ViewFactory(capacity, renderer, appViewHydrator), - [VIEW_POOL_CAPACITY, Renderer, AppViewHydrator] + (capacity, renderer) => new ViewFactory(capacity, renderer), + [VIEW_POOL_CAPACITY, Renderer] ), bind(VIEW_POOL_CAPACITY).toValue(10000), AppViewHydrator, diff --git a/modules/angular2/src/core/compiler/compiler.js b/modules/angular2/src/core/compiler/compiler.js index eaabf7527b..4a9cc68d2c 100644 --- a/modules/angular2/src/core/compiler/compiler.js +++ b/modules/angular2/src/core/compiler/compiler.js @@ -114,14 +114,21 @@ export class Compiler { } var template = this._templateResolver.resolve(component); - var directives = ListWrapper.map( - this._flattenDirectives(template), - (directive) => this._bindDirective(directive) - ); - var renderTemplate = this._buildRenderTemplate(component, template, directives); - pvPromise = this._renderer.compile(renderTemplate).then( (renderPv) => { - return this._compileNestedProtoViews(componentBinding, renderPv, directives, true); - }); + if (isPresent(template.renderer)) { + var directives = []; + pvPromise = this._renderer.createImperativeComponentProtoView(template.renderer).then( (renderPv) => { + return this._compileNestedProtoViews(componentBinding, renderPv, directives, true); + }); + } else { + var directives = ListWrapper.map( + this._flattenDirectives(template), + (directive) => this._bindDirective(directive) + ); + var renderTemplate = this._buildRenderTemplate(component, template, directives); + pvPromise = this._renderer.compile(renderTemplate).then( (renderPv) => { + return this._compileNestedProtoViews(componentBinding, renderPv, directives, true); + }); + } MapWrapper.set(this._compiling, component, pvPromise); return pvPromise; diff --git a/modules/angular2/src/core/compiler/dynamic_component_loader.js b/modules/angular2/src/core/compiler/dynamic_component_loader.js index 67b1827a29..c838bee0c2 100644 --- a/modules/angular2/src/core/compiler/dynamic_component_loader.js +++ b/modules/angular2/src/core/compiler/dynamic_component_loader.js @@ -70,15 +70,10 @@ export class DynamicComponentLoader { return this._compiler.compile(type).then(componentProtoView => { var componentView = this._viewFactory.getView(componentProtoView); - var hostView = location.hostView; this._viewHydrator.hydrateDynamicComponentView( - hostView, location.boundElementIndex, componentView, componentBinding, injector); + location, componentView, componentBinding, injector); - // TODO(vsavkin): return a component ref that dehydrates the component view and removes it - // from the component child views - // See ViewFactory.returnView - // See AppViewHydrator.dehydrateDynamicComponentView - var dispose = () => {throw "Not implemented";}; + var dispose = () => {throw new BaseException("Not implemented");}; return new ComponentRef(location, location.elementInjector.getDynamicallyLoadedComponent(), componentView, dispose); }); } @@ -87,19 +82,22 @@ export class DynamicComponentLoader { * Loads a component in the element specified by elementOrSelector. The loaded component receives * injection normally as a hosted view. */ - loadIntoNewLocation(elementOrSelector:any, type:Type, injector:Injector = null):Promise { + loadIntoNewLocation(type:Type, parentComponentLocation:ElementRef, elementOrSelector:any, + injector:Injector = null):Promise { this._assertTypeIsComponent(type); return this._compiler.compileInHost(type).then(hostProtoView => { var hostView = this._viewFactory.getView(hostProtoView); - this._viewHydrator.hydrateInPlaceHostView(null, elementOrSelector, hostView, injector); + this._viewHydrator.hydrateInPlaceHostView( + parentComponentLocation, elementOrSelector, hostView, injector + ); - // TODO(vsavkin): return a component ref that dehydrates the host view - // See ViewFactory.returnView - // See AppViewHydrator.dehydrateInPlaceHostView var newLocation = hostView.elementInjectors[0].getElementRef(); var component = hostView.elementInjectors[0].getComponent(); - var dispose = () => {throw "Not implemented";}; + var dispose = () => { + this._viewHydrator.dehydrateInPlaceHostView(parentComponentLocation, hostView); + this._viewFactory.returnView(hostView); + }; return new ComponentRef(newLocation, component, hostView.componentChildViews[0], dispose); }); } diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index 94054a914d..99b0de7db0 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -26,27 +26,21 @@ var _staticKeys; * @exportedAs angular2/view */ export class ElementRef { + hostView:viewModule.AppView; + boundElementIndex:number; + injector:Injector; elementInjector:ElementInjector; - constructor(elementInjector:ElementInjector){ + constructor(elementInjector, hostView, boundElementIndex, injector){ this.elementInjector = elementInjector; - } - - get hostView() { - return this.elementInjector._preBuiltObjects.view; + this.hostView = hostView; + this.boundElementIndex = boundElementIndex; + this.injector = injector; } get viewContainer() { return this.hostView.getOrCreateViewContainer(this.boundElementIndex); } - - get injector() { - return this.elementInjector._lightDomAppInjector; - } - - get boundElementIndex() { - return this.elementInjector._proto.index; - } } class StaticKeys { @@ -673,7 +667,7 @@ export class ElementInjector extends TreeNode { } getElementRef() { - return new ElementRef(this); + return new ElementRef(this, this._preBuiltObjects.view, this._proto.index, this._lightDomAppInjector); } getDynamicallyLoadedComponent() { diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index 272bcad939..93995daa2e 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -25,6 +25,9 @@ export class AppView { elementInjectors:List; changeDetector:ChangeDetector; componentChildViews: List; + /// Host views that were added by an imperative view. + /// This is a dynamically growing / shrinking array. + imperativeHostViews: List; viewContainers: List; preBuiltObjects: List; proto: AppProtoView; @@ -46,7 +49,7 @@ export class AppView { */ locals:Locals; - constructor(renderer:renderApi.Renderer, viewFactory:vfModule.ViewFactory, viewHydrator:vhModule.AppViewHydrator, proto:AppProtoView, protoLocals:Map) { + constructor(renderer:renderApi.Renderer, viewFactory:vfModule.ViewFactory, proto:AppProtoView, protoLocals:Map) { this.render = null; this.proto = proto; this.changeDetector = null; @@ -59,7 +62,8 @@ export class AppView { this.locals = new Locals(null, MapWrapper.clone(protoLocals)); //TODO optimize this this.renderer = renderer; this.viewFactory = viewFactory; - this.viewHydrator = viewHydrator; + this.viewHydrator = null; + this.imperativeHostViews = []; } init(changeDetector:ChangeDetector, elementInjectors:List, rootElementInjectors:List, @@ -151,7 +155,7 @@ export class AppView { } var result = expr.eval(context, new Locals(this.locals, locals)); if (isPresent(result)) { - allowDefaultBehavior = allowDefaultBehavior && result; + allowDefaultBehavior = allowDefaultBehavior && result; } }); } diff --git a/modules/angular2/src/core/compiler/view_factory.js b/modules/angular2/src/core/compiler/view_factory.js index dd5612ed51..0f43020436 100644 --- a/modules/angular2/src/core/compiler/view_factory.js +++ b/modules/angular2/src/core/compiler/view_factory.js @@ -5,7 +5,6 @@ import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; import {NgElement} from 'angular2/src/core/compiler/ng_element'; import * as viewModule from './view'; import {Renderer} from 'angular2/src/render/api'; -import {AppViewHydrator} from './view_hydrator'; // TODO(tbosch): Make this an OpaqueToken as soon as our transpiler supports this! export const VIEW_POOL_CAPACITY = 'ViewFactory.viewPoolCapacity'; @@ -15,13 +14,11 @@ export class ViewFactory { _poolCapacityPerProtoView:number; _pooledViewsPerProtoView:Map>; _renderer:Renderer; - _viewHydrator:AppViewHydrator; - constructor(@Inject(VIEW_POOL_CAPACITY) poolCapacityPerProtoView, renderer:Renderer, viewHydrator:AppViewHydrator) { + constructor(@Inject(VIEW_POOL_CAPACITY) poolCapacityPerProtoView, renderer:Renderer) { this._poolCapacityPerProtoView = poolCapacityPerProtoView; this._pooledViewsPerProtoView = MapWrapper.create(); this._renderer = renderer; - this._viewHydrator = viewHydrator; } getView(protoView:viewModule.AppProtoView):viewModule.AppView { @@ -48,7 +45,7 @@ export class ViewFactory { } _createView(protoView:viewModule.AppProtoView): viewModule.AppView { - var view = new viewModule.AppView(this._renderer, this, this._viewHydrator, protoView, protoView.protoLocals); + var view = new viewModule.AppView(this._renderer, this, protoView, protoView.protoLocals); var changeDetector = protoView.protoChangeDetector.instantiate(view, protoView.bindings, protoView.getVariableBindings(), protoView.getdirectiveRecords()); diff --git a/modules/angular2/src/core/compiler/view_hydrator.js b/modules/angular2/src/core/compiler/view_hydrator.js index 59055d0faa..2e180c51a6 100644 --- a/modules/angular2/src/core/compiler/view_hydrator.js +++ b/modules/angular2/src/core/compiler/view_hydrator.js @@ -7,6 +7,7 @@ import * as viewModule from './view'; import {BindingPropagationConfig, Locals} from 'angular2/change_detection'; import * as renderApi from 'angular2/src/render/api'; +import {ViewFactory} from 'angular2/src/core/compiler/view_factory'; /** * A dehydrated view is a state of the view that allows it to be moved around @@ -27,13 +28,17 @@ import * as renderApi from 'angular2/src/render/api'; @Injectable() export class AppViewHydrator { _renderer:renderApi.Renderer; + _viewFactory:ViewFactory; - constructor(renderer:renderApi.Renderer) { + constructor(renderer:renderApi.Renderer, viewFactory:ViewFactory) { this._renderer = renderer; + this._viewFactory = viewFactory; } - hydrateDynamicComponentView(hostView:viewModule.AppView, boundElementIndex:number, + hydrateDynamicComponentView(location:eli.ElementRef, componentView:viewModule.AppView, componentDirective:eli.DirectiveBinding, injector:Injector) { + var hostView = location.hostView; + var boundElementIndex = location.boundElementIndex; var binder = hostView.proto.elementBinders[boundElementIndex]; if (!binder.hasDynamicComponent()) { throw new BaseException(`There is no dynamic component directive at element ${boundElementIndex}`); @@ -84,16 +89,23 @@ export class AppViewHydrator { // parentView.componentChildViews[boundElementIndex] = null; } - hydrateInPlaceHostView(parentView:viewModule.AppView, hostElementSelector, hostView:viewModule.AppView, injector:Injector) { + hydrateInPlaceHostView(parentComponentLocation:eli.ElementRef, + hostElementSelector, hostView:viewModule.AppView, injector:Injector) { var parentRenderViewRef = null; - if (isPresent(parentView)) { - // Needed for user views - throw new BaseException('Not yet supported'); + if (isPresent(parentComponentLocation)) { + var parentView = parentComponentLocation.hostView.componentChildViews[parentComponentLocation.boundElementIndex]; + parentRenderViewRef = parentView.render; + parentView.changeDetector.addChild(hostView.changeDetector); + ListWrapper.push(parentView.imperativeHostViews, hostView); + + if (isBlank(injector)) { + injector = parentComponentLocation.injector; + } } + var binder = hostView.proto.elementBinders[0]; var shadowDomAppInjector = this._createShadowDomAppInjector(binder.componentDirective, injector); - // render views var renderViewRefs = this._renderer.createInPlaceHostView(parentRenderViewRef, hostElementSelector, hostView.proto.render); this._viewHydrateRecurse( @@ -101,11 +113,13 @@ export class AppViewHydrator { ); } - dehydrateInPlaceHostView(parentView:viewModule.AppView, hostView:viewModule.AppView) { + dehydrateInPlaceHostView(parentComponentLocation:eli.ElementRef, hostView:viewModule.AppView) { var parentRenderViewRef = null; - if (isPresent(parentView)) { - // Needed for user views - throw new BaseException('Not yet supported'); + if (isPresent(parentComponentLocation)) { + var parentView = parentComponentLocation.hostView.componentChildViews[parentComponentLocation.boundElementIndex]; + parentRenderViewRef = parentView.render; + ListWrapper.remove(parentView.imperativeHostViews, hostView); + parentView.changeDetector.removeChild(hostView.changeDetector); } var render = hostView.render; this._viewDehydrateRecurse(hostView); @@ -137,7 +151,7 @@ export class AppViewHydrator { appInjector: Injector, hostElementInjector: eli.ElementInjector, context: Object, locals:Locals):number { if (view.hydrated()) throw new BaseException('The view is already hydrated.'); - + view.viewHydrator = this; view.render = renderComponentViewRefs[renderComponentIndex++]; view.context = context; @@ -215,12 +229,22 @@ export class AppViewHydrator { this._viewDehydrateRecurse(componentView); var binder = view.proto.elementBinders[i]; if (binder.hasDynamicComponent()) { - view.componentChildViews[i] = null; view.changeDetector.removeShadowDomChild(componentView.changeDetector); + view.componentChildViews[i] = null; + this._viewFactory.returnView(componentView); } } } + // imperativeHostViews + for (var i = 0; i < view.imperativeHostViews.length; i++) { + var hostView = view.imperativeHostViews[i]; + this._viewDehydrateRecurse(hostView); + view.changeDetector.removeChild(hostView.changeDetector); + this._viewFactory.returnView(hostView); + } + view.imperativeHostViews = []; + // elementInjectors for (var i = 0; i < view.elementInjectors.length; i++) { if (isPresent(view.elementInjectors[i])) { diff --git a/modules/angular2/src/dom/browser_adapter.dart b/modules/angular2/src/dom/browser_adapter.dart index 3ac8d50d79..fca1942793 100644 --- a/modules/angular2/src/dom/browser_adapter.dart +++ b/modules/angular2/src/dom/browser_adapter.dart @@ -151,7 +151,7 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter { void appendChild(Node el, Node node) { el.append(node); } - void removeChild(Element el, Node node) { + void removeChild(el, Node node) { node.remove(); } void replaceChild(Node el, Node newNode, Node oldNode) { diff --git a/modules/angular2/src/render/api.js b/modules/angular2/src/render/api.js index fa51ab11ff..efec2f0c49 100644 --- a/modules/angular2/src/render/api.js +++ b/modules/angular2/src/render/api.js @@ -165,6 +165,14 @@ export class Renderer { */ createHostProtoView(componentId):Promise { return null; } + /** + * Creats a ProtoViewDto for a component that will use an imperative View using the given + * renderer. + * Note: Rigth now, the renderer argument is ignored, but will be used in the future to define + * a custom handler. + */ + createImperativeComponentProtoView(rendererId):Promise { return null; } + /** * Compiles a single RenderProtoView. Non recursive so that * we don't need to serialize all possible components over the wire, diff --git a/modules/angular2/src/render/dom/direct_dom_renderer.js b/modules/angular2/src/render/dom/direct_dom_renderer.js index 76bfd52286..461dc823f2 100644 --- a/modules/angular2/src/render/dom/direct_dom_renderer.js +++ b/modules/angular2/src/render/dom/direct_dom_renderer.js @@ -12,6 +12,7 @@ import {Compiler} from './compiler/compiler'; import {ShadowDomStrategy} from './shadow_dom/shadow_dom_strategy'; import {ProtoViewBuilder} from './view/proto_view_builder'; import {DOM} from 'angular2/src/dom/dom_adapter'; +import {ViewContainer} from './view/view_container'; function _resolveViewContainer(vc:api.ViewContainerRef) { return _resolveView(vc.view).getOrCreateViewContainer(vc.elementIndex); @@ -90,6 +91,12 @@ export class DirectDomRenderer extends api.Renderer { return PromiseWrapper.resolve(hostProtoViewBuilder.build()); } + createImperativeComponentProtoView(rendererId):Promise { + var protoViewBuilder = new ProtoViewBuilder(null); + protoViewBuilder.setImperativeRendererId(rendererId); + return PromiseWrapper.resolve(protoViewBuilder.build()); + } + compile(template:api.ViewDefinition):Promise { // Note: compiler already uses a DirectDomProtoViewRef, so we don't // need to do anything here @@ -156,6 +163,21 @@ export class DirectDomRenderer extends api.Renderer { this._viewHydrator.dehydrateInPlaceHostView(parentView, hostView); } + setImperativeComponentRootNodes(parentViewRef:api.ViewRef, elementIndex:number, nodes:List):void { + var parentView = _resolveView(parentViewRef); + var hostElement = parentView.boundElements[elementIndex]; + var componentView = parentView.componentChildViews[elementIndex]; + if (isBlank(componentView)) { + throw new BaseException(`There is no componentChildView at index ${elementIndex}`); + } + if (isBlank(componentView.proto.imperativeRendererId)) { + throw new BaseException(`This component view has no imperative renderer`); + } + ViewContainer.removeViewNodes(componentView); + componentView.rootNodes = nodes; + this._shadowDomStrategy.attachTemplate(hostElement, componentView); + } + setElementProperty(viewRef:api.ViewRef, elementIndex:number, propertyName:string, propertyValue:any):void { _resolveView(viewRef).setElementProperty(elementIndex, propertyName, propertyValue); } diff --git a/modules/angular2/src/render/dom/view/proto_view.js b/modules/angular2/src/render/dom/view/proto_view.js index 714734a39c..4a76159d66 100644 --- a/modules/angular2/src/render/dom/view/proto_view.js +++ b/modules/angular2/src/render/dom/view/proto_view.js @@ -11,15 +11,23 @@ export class RenderProtoView { elementBinders:List; isTemplateElement:boolean; rootBindingOffset:int; + imperativeRendererId:string; constructor({ elementBinders, - element + element, + imperativeRendererId }) { this.element = element; this.elementBinders = elementBinders; - this.isTemplateElement = DOM.isTemplateElement(this.element); - this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0; + this.imperativeRendererId = imperativeRendererId; + if (isPresent(imperativeRendererId)) { + this.rootBindingOffset = 0; + this.isTemplateElement = false; + } else { + this.isTemplateElement = DOM.isTemplateElement(this.element); + this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0; + } } mergeChildComponentProtoViews(componentProtoViews:List) { diff --git a/modules/angular2/src/render/dom/view/proto_view_builder.js b/modules/angular2/src/render/dom/view/proto_view_builder.js index b458c6bbb2..c058df1528 100644 --- a/modules/angular2/src/render/dom/view/proto_view_builder.js +++ b/modules/angular2/src/render/dom/view/proto_view_builder.js @@ -20,12 +20,18 @@ export class ProtoViewBuilder { rootElement; variableBindings: Map; elements:List; - isRootView:boolean; + imperativeRendererId:string; constructor(rootElement) { this.rootElement = rootElement; this.elements = []; this.variableBindings = MapWrapper.create(); + this.imperativeRendererId = null; + } + + setImperativeRendererId(id:string):ProtoViewBuilder { + this.imperativeRendererId = id; + return this; } bindElement(element, description = null):ElementBinderBuilder { @@ -90,7 +96,8 @@ export class ProtoViewBuilder { return new api.ProtoViewDto({ render: new directDomRenderer.DirectDomProtoViewRef(new RenderProtoView({ element: this.rootElement, - elementBinders: renderElementBinders + elementBinders: renderElementBinders, + imperativeRendererId: this.imperativeRendererId })), elementBinders: apiElementBinders, variableBindings: this.variableBindings diff --git a/modules/angular2/src/render/dom/view/view.js b/modules/angular2/src/render/dom/view/view.js index 1510129dcf..291f96dec7 100644 --- a/modules/angular2/src/render/dom/view/view.js +++ b/modules/angular2/src/render/dom/view/view.js @@ -31,6 +31,9 @@ export class RenderView { hydrated: boolean; _eventDispatcher: any/*EventDispatcher*/; eventHandlerRemovers: List; + /// Host views that were added by an imperative view. + /// This is a dynamically growing / shrinking array. + imperativeHostViews: List; constructor( proto:RenderProtoView, rootNodes:List, @@ -46,7 +49,8 @@ export class RenderView { this.componentChildViews = ListWrapper.createFixedSize(boundElements.length); this.hostLightDom = null; this.hydrated = false; - this.eventHandlerRemovers = null; + this.eventHandlerRemovers = []; + this.imperativeHostViews = []; } getDirectParentLightDom(boundElementIndex:number) { diff --git a/modules/angular2/src/render/dom/view/view_factory.js b/modules/angular2/src/render/dom/view/view_factory.js index 6a6b9fce49..2fc92a4d3c 100644 --- a/modules/angular2/src/render/dom/view/view_factory.js +++ b/modules/angular2/src/render/dom/view/view_factory.js @@ -58,6 +58,12 @@ export class ViewFactory { } _createView(protoView:pvModule.RenderProtoView, inplaceElement): viewModule.RenderView { + if (isPresent(protoView.imperativeRendererId)) { + return new viewModule.RenderView( + protoView, [], [], [], [] + ); + } + var rootElementClone = isPresent(inplaceElement) ? inplaceElement : DOM.importIntoDoc(protoView.element); var elementsWithBindingsDynamic; if (protoView.isTemplateElement) { @@ -125,7 +131,7 @@ export class ViewFactory { // static child components if (binder.hasStaticComponent()) { var childView = this._createView(binder.nestedProtoView, null); - this.setComponentView(view, binderIdx, childView); + ViewFactory.setComponentView(this._shadowDomStrategy, view, binderIdx, childView); } // events @@ -148,10 +154,10 @@ export class ViewFactory { // This method is used by the ViewFactory and the ViewHydrator // TODO(tbosch): change shadow dom emulation so that LightDom // instances don't need to be recreated by instead hydrated/dehydrated - setComponentView(hostView:viewModule.RenderView, elementIndex:number, componentView:viewModule.RenderView) { + static setComponentView(shadowDomStrategy:ShadowDomStrategy, hostView:viewModule.RenderView, elementIndex:number, componentView:viewModule.RenderView) { var element = hostView.boundElements[elementIndex]; - var lightDom = this._shadowDomStrategy.constructLightDom(hostView, componentView, element); - this._shadowDomStrategy.attachTemplate(element, componentView); + var lightDom = shadowDomStrategy.constructLightDom(hostView, componentView, element); + shadowDomStrategy.attachTemplate(element, componentView); hostView.lightDoms[elementIndex] = lightDom; hostView.componentChildViews[elementIndex] = componentView; } diff --git a/modules/angular2/src/render/dom/view/view_hydrator.js b/modules/angular2/src/render/dom/view/view_hydrator.js index 39aaadf07b..98f7f58f98 100644 --- a/modules/angular2/src/render/dom/view/view_hydrator.js +++ b/modules/angular2/src/render/dom/view/view_hydrator.js @@ -7,6 +7,7 @@ import {EventManager} from '../events/event_manager'; import {ViewFactory} from './view_factory'; import * as vcModule from './view_container'; import * as viewModule from './view'; +import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy'; /** * A dehydrated view is a state of the view that allows it to be moved around @@ -23,14 +24,16 @@ import * as viewModule from './view'; export class RenderViewHydrator { _eventManager:EventManager; _viewFactory:ViewFactory; + _shadowDomStrategy:ShadowDomStrategy; - constructor(eventManager:EventManager, viewFactory:ViewFactory) { + constructor(eventManager:EventManager, viewFactory:ViewFactory, shadowDomStrategy:ShadowDomStrategy) { this._eventManager = eventManager; this._viewFactory = viewFactory; + this._shadowDomStrategy = shadowDomStrategy; } hydrateDynamicComponentView(hostView:viewModule.RenderView, boundElementIndex:number, componentView:viewModule.RenderView) { - this._viewFactory.setComponentView(hostView, boundElementIndex, componentView); + ViewFactory.setComponentView(this._shadowDomStrategy, hostView, boundElementIndex, componentView); var lightDom = hostView.lightDoms[boundElementIndex]; this._viewHydrateRecurse(componentView, lightDom); if (isPresent(lightDom)) { @@ -50,15 +53,17 @@ export class RenderViewHydrator { hydrateInPlaceHostView(parentView:viewModule.RenderView, hostView:viewModule.RenderView) { if (isPresent(parentView)) { - throw new BaseException('Not supported yet'); + ListWrapper.push(parentView.imperativeHostViews, hostView); } this._viewHydrateRecurse(hostView, null); } dehydrateInPlaceHostView(parentView:viewModule.RenderView, hostView:viewModule.RenderView) { if (isPresent(parentView)) { - throw new BaseException('Not supported yet'); + ListWrapper.remove(parentView.imperativeHostViews, hostView); } + vcModule.ViewContainer.removeViewNodes(hostView); + hostView.rootNodes = []; this._viewDehydrateRecurse(hostView); } @@ -130,12 +135,24 @@ export class RenderViewHydrator { this._viewDehydrateRecurse(cv); if (view.proto.elementBinders[i].hasDynamicComponent()) { vcModule.ViewContainer.removeViewNodes(cv); + this._viewFactory.returnView(cv); view.lightDoms[i] = null; view.componentChildViews[i] = null; } } } + // imperativeHostViews + for (var i = 0; i < view.imperativeHostViews.length; i++) { + var hostView = view.imperativeHostViews[i]; + this._viewDehydrateRecurse(hostView); + vcModule.ViewContainer.removeViewNodes(hostView); + hostView.rootNodes = []; + this._viewFactory.returnView(hostView); + } + view.imperativeHostViews = []; + + // viewContainers and content tags if (isPresent(view.viewContainers)) { for (var i = 0; i < view.viewContainers.length; i++) { diff --git a/modules/angular2/src/test_lib/test_injector.js b/modules/angular2/src/test_lib/test_injector.js index 62ebfea936..9584366dfc 100644 --- a/modules/angular2/src/test_lib/test_injector.js +++ b/modules/angular2/src/test_lib/test_injector.js @@ -79,6 +79,7 @@ function _getAppBindings() { bind(ShadowDomStrategy).toFactory( (styleUrlResolver, doc) => new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, doc.head), [StyleUrlResolver, appDocumentToken]), + bind(DirectDomRenderer).toClass(DirectDomRenderer), bind(Renderer).toClass(DirectDomRenderer), bind(rc.Compiler).toClass(rc.DefaultCompiler), rvf.ViewFactory, diff --git a/modules/angular2/test/core/compiler/compiler_spec.js b/modules/angular2/test/core/compiler/compiler_spec.js index 71d708b762..a08af50f29 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.js +++ b/modules/angular2/test/core/compiler/compiler_spec.js @@ -10,10 +10,11 @@ import { inject, IS_DARTIUM, it, + SpyObject, proxy } from 'angular2/test_lib'; import {List, ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; -import {Type, isBlank, stringify, isPresent} from 'angular2/src/facade/lang'; +import {IMPLEMENTS, Type, isBlank, stringify, isPresent} from 'angular2/src/facade/lang'; import {PromiseWrapper, Promise} from 'angular2/src/facade/async'; import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler'; @@ -30,20 +31,28 @@ import {ProtoViewFactory} from 'angular2/src/core/compiler/proto_view_factory'; import {UrlResolver} from 'angular2/src/services/url_resolver'; import * as renderApi from 'angular2/src/render/api'; +// TODO(tbosch): Spys don't support named modules... +import {Renderer} from 'angular2/src/render/api'; export function main() { describe('compiler', function() { - var reader, tplResolver, renderer, protoViewFactory, cmpUrlMapper; + var reader, tplResolver, renderer, protoViewFactory, cmpUrlMapper, renderCompileRequests; beforeEach(() => { reader = new DirectiveMetadataReader(); tplResolver = new FakeTemplateResolver(); cmpUrlMapper = new RuntimeComponentUrlMapper(); + renderer = new SpyRenderer(); }); function createCompiler(renderCompileResults:List, protoViewFactoryResults:List) { var urlResolver = new FakeUrlResolver(); - renderer = new FakeRenderer(renderCompileResults); + renderCompileRequests = []; + renderer.spy('compile').andCallFake( (template) => { + ListWrapper.push(renderCompileRequests, template); + return PromiseWrapper.resolve(ListWrapper.removeAt(renderCompileResults, 0)); + }); + protoViewFactory = new FakeProtoViewFactory(protoViewFactoryResults) return new Compiler( reader, @@ -62,8 +71,8 @@ export function main() { tplResolver.setView(MainComponent, template); var compiler = createCompiler([createRenderProtoView()], [createProtoView()]); return compiler.compile(MainComponent).then( (protoView) => { - expect(renderer.requests.length).toBe(1); - return renderer.requests[0]; + expect(renderCompileRequests.length).toBe(1); + return renderCompileRequests[0]; }); } @@ -362,6 +371,11 @@ export function main() { })); it('should create host proto views', inject([AsyncTestCompleter], (async) => { + renderer.spy('createHostProtoView').andCallFake( (componentId) => { + return PromiseWrapper.resolve( + createRenderProtoView([createRenderComponentElementBinder(0)]) + ); + }); tplResolver.setView(MainComponent, new View({template: '
'})); var rootProtoView = createProtoView([ createComponentElementBinder(reader, MainComponent) @@ -379,6 +393,25 @@ export function main() { async.done(); }); })); + + it('should create imperative proto views', inject([AsyncTestCompleter], (async) => { + renderer.spy('createImperativeComponentProtoView').andCallFake( (rendererId) => { + return PromiseWrapper.resolve( + createRenderProtoView([]) + ); + }); + tplResolver.setView(MainComponent, new View({renderer: 'some-renderer'})); + var mainProtoView = createProtoView(); + var compiler = createCompiler( + [], + [mainProtoView] + ); + compiler.compile(MainComponent).then( (protoView) => { + expect(protoView).toBe(mainProtoView); + expect(renderer.spy('createImperativeComponentProtoView')).toHaveBeenCalledWith('some-renderer'); + async.done(); + }); + })); }); } @@ -482,26 +515,11 @@ class DirectiveWithAttributes { constructor(@Attribute('someAttr') someAttr:string) {} } -class FakeRenderer extends renderApi.Renderer { - requests:List; - _results:List; - - constructor(results) { - super(); - this._results = results; - this.requests = []; - } - - compile(template:renderApi.ViewDefinition):Promise { - ListWrapper.push(this.requests, template); - return PromiseWrapper.resolve(ListWrapper.removeAt(this._results, 0)); - } - - createHostProtoView(componentId):Promise { - return PromiseWrapper.resolve( - createRenderProtoView([createRenderComponentElementBinder(0)]) - ); - } +@proxy +@IMPLEMENTS(Renderer) +class SpyRenderer extends SpyObject { + constructor(){super(Renderer);} + noSuchMethod(m){return super.noSuchMethod(m)} } class FakeUrlResolver extends UrlResolver { diff --git a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js index d6a6056473..ac93c2157b 100644 --- a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js +++ b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js @@ -21,6 +21,7 @@ import {View} from 'angular2/src/core/annotations/view'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; import {ElementRef} from 'angular2/src/core/compiler/element_injector'; import {If} from 'angular2/src/directives/if'; +import {DirectDomRenderer} from 'angular2/src/render/dom/direct_dom_renderer'; export function main() { describe('DynamicComponentLoader', function () { @@ -134,9 +135,71 @@ export function main() { }); })); }); + + describe('loading into a new location', () => { + it('should allow to create, update and destroy components', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new View({ + template: '', + directives: [ImperativeViewComponentUsingNgComponent] + })); + tb.createView(MyComp).then((view) => { + var userViewComponent = view.rawView.locals.get("impview"); + + userViewComponent.done.then((childComponentRef) => { + view.detectChanges(); + + expect(view.rootNodes).toHaveText('hello'); + + childComponentRef.instance.ctxProp = 'new'; + + view.detectChanges(); + + expect(view.rootNodes).toHaveText('new'); + + childComponentRef.dispose(); + + expect(view.rootNodes).toHaveText(''); + + async.done(); + }); + }); + })); + + }); + }); } +@Component({ + selector: 'imp-ng-cmp' +}) +@View({ + renderer: 'imp-ng-cmp-renderer' +}) +class ImperativeViewComponentUsingNgComponent { + done; + + constructor(self:ElementRef, dynamicComponentLoader:DynamicComponentLoader, renderer:DirectDomRenderer) { + var div = el('
'); + renderer.setImperativeComponentRootNodes(self.hostView.render, self.boundElementIndex, [div]); + this.done = dynamicComponentLoader.loadIntoNewLocation(ChildComp, self, div, null); + } +} + +@Component({ + selector: 'child-cmp', +}) +@View({ + template: '{{ctxProp}}' +}) +class ChildComp { + ctxProp:string; + constructor() { + this.ctxProp = 'hello'; + } +} + class DynamicallyCreatedComponentService { } diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index e61cc2fa79..b4e2624d95 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -714,7 +714,7 @@ export function main() { beforeEach( () => { renderer = new FakeRenderer(); var protoView = new AppProtoView(null, null); - view = new AppView(renderer, null, null, protoView, MapWrapper.create()); + view = new AppView(renderer, null, protoView, MapWrapper.create()); view.render = new ViewRef(); }); diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 974438c282..f60da12e5a 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -33,6 +33,9 @@ import {If} from 'angular2/src/directives/if'; import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {Compiler} from 'angular2/src/core/compiler/compiler'; +import {ElementRef} from 'angular2/src/core/compiler/element_injector'; + +import {DirectDomRenderer} from 'angular2/src/render/dom/direct_dom_renderer'; export function main() { describe('integration tests', function() { @@ -602,7 +605,7 @@ export function main() { DOM.dispatchEvent(view.rootNodes[1], DOM.createMouseEvent('click')); expect(DOM.getChecked(view.rootNodes[0])).toBeFalsy(); expect(DOM.getChecked(view.rootNodes[1])).toBeTruthy(); - async.done(); + async.done(); }); })); @@ -722,6 +725,18 @@ export function main() { })); }); + it('should support imperative views', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new View({ + template: '', + directives: [SimpleImperativeViewComponent] + })); + tb.createView(MyComp).then((view) => { + expect(view.rootNodes).toHaveText('hello imp view'); + async.done(); + }); + })); + // Disabled until a solution is found, refs: // - https://github.com/angular/angular/issues/776 // - https://github.com/angular/angular/commit/81f3f32 @@ -783,6 +798,21 @@ export function main() { }); } +@Component({ + selector: 'simple-imp-cmp' +}) +@View({ + renderer: 'simple-imp-cmp-renderer' +}) +class SimpleImperativeViewComponent { + done; + + constructor(self:ElementRef, renderer:DirectDomRenderer) { + renderer.setImperativeComponentRootNodes(self.hostView.render, self.boundElementIndex, [el('hello imp view')]); + } +} + + @Decorator({ selector: 'dynamic-vp' }) @@ -888,7 +918,7 @@ class ComponentWithPipes { @Component({ selector: 'child-cmp', - injectables: [MyService] + injectables: [MyService], }) @View({ directives: [MyDir], diff --git a/modules/angular2/test/core/compiler/view_factory_spec.js b/modules/angular2/test/core/compiler/view_factory_spec.js index 7bea67953c..43194c162b 100644 --- a/modules/angular2/test/core/compiler/view_factory_spec.js +++ b/modules/angular2/test/core/compiler/view_factory_spec.js @@ -35,7 +35,7 @@ export function main() { }); function createViewFactory({capacity}):ViewFactory { - return new ViewFactory(capacity, renderer, null); + return new ViewFactory(capacity, renderer); } function createProtoChangeDetector() { diff --git a/modules/angular2/test/core/compiler/view_hydrator_spec.js b/modules/angular2/test/core/compiler/view_hydrator_spec.js index 20e4dbe72c..65c0286dbd 100644 --- a/modules/angular2/test/core/compiler/view_hydrator_spec.js +++ b/modules/angular2/test/core/compiler/view_hydrator_spec.js @@ -21,21 +21,24 @@ import {AppProtoView, AppView} from 'angular2/src/core/compiler/view'; import {Renderer, ViewRef} from 'angular2/src/render/api'; import {ChangeDetector} from 'angular2/change_detection'; import {ElementBinder} from 'angular2/src/core/compiler/element_binder'; -import {DirectiveBinding, ElementInjector} from 'angular2/src/core/compiler/element_injector'; +import {DirectiveBinding, ElementInjector, ElementRef} from 'angular2/src/core/compiler/element_injector'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; import {Component} from 'angular2/src/core/annotations/annotations'; import {AppViewHydrator} from 'angular2/src/core/compiler/view_hydrator'; +import {ViewFactory} from 'angular2/src/core/compiler/view_factory'; export function main() { describe('AppViewHydrator', () => { var renderer; var reader; var hydrator; + var viewFactory; beforeEach( () => { renderer = new SpyRenderer(); reader = new DirectiveMetadataReader(); - hydrator = new AppViewHydrator(renderer); + viewFactory = new SpyViewFactory(); + hydrator = new AppViewHydrator(renderer, viewFactory); }); function createDirectiveBinding(type) { @@ -81,14 +84,14 @@ export function main() { } function createEmptyView() { - var view = new AppView(renderer, null, null, createProtoView(), MapWrapper.create()); + var view = new AppView(renderer, null, createProtoView(), MapWrapper.create()); var changeDetector = new SpyChangeDetector(); view.init(changeDetector, [], [], [], []); return view; } function createHostView(pv, shadowView, componentInstance, elementInjectors = null) { - var view = new AppView(renderer, null, null, pv, MapWrapper.create()); + var view = new AppView(renderer, null, pv, MapWrapper.create()); var changeDetector = new SpyChangeDetector(); var eis; @@ -117,7 +120,7 @@ export function main() { var view = createHostView(pv, null, null); var shadowView = createEmptyView(); expect( - () => hydrator.hydrateDynamicComponentView(view, 0, shadowView, null, null) + () => hydrator.hydrateDynamicComponentView(new ElementRef(null, view, 0, null), shadowView, null, null) ).toThrowError('There is no dynamic component directive at element 0'); }); @@ -126,7 +129,7 @@ export function main() { var view = createHostView(pv, null, null); var shadowView = createEmptyView(); expect( - () => hydrator.hydrateDynamicComponentView(view, 0, shadowView, null, null) + () => hydrator.hydrateDynamicComponentView(new ElementRef(null, view, 0, null), shadowView, null, null) ).toThrowError('There is no dynamic component directive at element 0'); }); @@ -135,9 +138,10 @@ export function main() { var shadowView = createEmptyView(); var view = createHostView(pv, null, null); renderer.spy('createDynamicComponentView').andReturn([new ViewRef(), new ViewRef()]); - hydrator.hydrateDynamicComponentView(view, 0, shadowView, createDirectiveBinding(SomeComponent), null); + var elRef = new ElementRef(null, view, 0, null); + hydrator.hydrateDynamicComponentView(elRef, shadowView, createDirectiveBinding(SomeComponent), null); expect( - () => hydrator.hydrateDynamicComponentView(view, 0, shadowView, null, null) + () => hydrator.hydrateDynamicComponentView(elRef, shadowView, null, null) ).toThrowError('There already is a bound component at element 0'); }); @@ -217,6 +221,7 @@ export function main() { expect(hostView.componentChildViews[0]).toBe(shadowView); expect(hostView.changeDetector.spy('removeShadowDomChild')).not.toHaveBeenCalled(); + expect(viewFactory.spy('returnView')).not.toHaveBeenCalled(); }); it('should clear dynamic child components', () => { @@ -225,6 +230,19 @@ export function main() { expect(hostView.componentChildViews[0]).toBe(null); expect(hostView.changeDetector.spy('removeShadowDomChild')).toHaveBeenCalledWith(shadowView.changeDetector); + expect(viewFactory.spy('returnView')).toHaveBeenCalledWith(shadowView); + }); + + it('should clear imperatively added child components', () => { + createAndHydrate(createProtoView()); + var impHostView = createHostView(createHostProtoView(createProtoView()), createEmptyView(), null); + shadowView.imperativeHostViews = [impHostView]; + + dehydrate(hostView); + + expect(shadowView.imperativeHostViews).toEqual([]); + expect(viewFactory.spy('returnView')).toHaveBeenCalledWith(impHostView); + expect(shadowView.changeDetector.spy('removeChild')).toHaveBeenCalledWith(impHostView.changeDetector); }); }); @@ -255,3 +273,10 @@ class SpyElementInjector extends SpyObject { constructor(){super(ElementInjector);} noSuchMethod(m){return super.noSuchMethod(m)} } + +@proxy +@IMPLEMENTS(ViewFactory) +class SpyViewFactory extends SpyObject { + constructor(){super(ViewFactory);} + noSuchMethod(m){return super.noSuchMethod(m)} +} \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/view_spec.js b/modules/angular2/test/core/compiler/view_spec.js index cca2735367..80d2d2d493 100644 --- a/modules/angular2/test/core/compiler/view_spec.js +++ b/modules/angular2/test/core/compiler/view_spec.js @@ -57,7 +57,7 @@ export function main() { } function createViewWithOneBoundElement(pv) { - var view = new AppView(renderer, null, null, pv, MapWrapper.create()); + var view = new AppView(renderer, null, pv, MapWrapper.create()); var changeDetector = new SpyChangeDetector(); var eij = createElementInjector(); view.init(changeDetector, [eij], [eij], diff --git a/modules/angular2/test/render/dom/direct_dom_renderer_integration_spec.js b/modules/angular2/test/render/dom/direct_dom_renderer_integration_spec.js index 89b6a56fc4..4ad91c09a9 100644 --- a/modules/angular2/test/render/dom/direct_dom_renderer_integration_spec.js +++ b/modules/angular2/test/render/dom/direct_dom_renderer_integration_spec.js @@ -50,6 +50,16 @@ export function main() { }); })); + it('should create imperative proto views', inject([AsyncTestCompleter], (async) => { + createRenderer(); + renderer.createImperativeComponentProtoView('someRenderId').then( (rootProtoView) => { + expect(rootProtoView.elementBinders).toEqual([]); + + expect(rootProtoView.render.delegate.imperativeRendererId).toBe('someRenderId'); + async.done(); + }); + })); + it('should add a static component', inject([AsyncTestCompleter], (async) => { createRenderer(); renderer.createHostProtoView('someComponentId').then( (rootProtoView) => { diff --git a/modules/angular2/test/render/dom/integration_testbed.js b/modules/angular2/test/render/dom/integration_testbed.js index 35e0a7a151..c12b996a03 100644 --- a/modules/angular2/test/render/dom/integration_testbed.js +++ b/modules/angular2/test/render/dom/integration_testbed.js @@ -46,7 +46,7 @@ export class IntegrationTestbed { this.eventPlugin = new FakeEventManagerPlugin(); var eventManager = new EventManager([this.eventPlugin], new FakeVmTurnZone()); var viewFactory = new ViewFactory(viewCacheCapacity, eventManager, shadowDomStrategy); - var viewHydrator = new RenderViewHydrator(eventManager, viewFactory); + var viewHydrator = new RenderViewHydrator(eventManager, viewFactory, shadowDomStrategy); this.renderer = new DirectDomRenderer(compiler, viewFactory, viewHydrator, shadowDomStrategy); } diff --git a/modules/angular2/test/render/dom/view/view_hydrator_spec.js b/modules/angular2/test/render/dom/view/view_hydrator_spec.js index 35b4101b1d..0e8247514f 100644 --- a/modules/angular2/test/render/dom/view/view_hydrator_spec.js +++ b/modules/angular2/test/render/dom/view/view_hydrator_spec.js @@ -14,7 +14,7 @@ import { xit, SpyObject, proxy } from 'angular2/test_lib'; -import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang'; +import {IMPLEMENTS, isBlank, isPresent} from 'angular2/src/facade/lang'; import {RenderProtoView} from 'angular2/src/render/dom/view/proto_view'; import {ElementBinder} from 'angular2/src/render/dom/view/element_binder'; @@ -76,7 +76,7 @@ export function main() { function createHostView(pv, shadowDomView) { var view = new RenderView(pv, [el('
')], [], [el('
')], [null]); - viewFactory.setComponentView(view, 0, shadowDomView); + ViewFactory.setComponentView(shadowDomStrategy, view, 0, shadowDomView); return view; } @@ -94,8 +94,8 @@ export function main() { shadowDomStrategy.spy('constructLightDom').andCallFake( (lightDomView, shadowDomView, el) => { return new SpyLightDom(); }); - viewFactory = new ViewFactory(1, eventManager, shadowDomStrategy); - viewHydrator = new RenderViewHydrator(eventManager, viewFactory); + viewFactory = new SpyViewFactory(); + viewHydrator = new RenderViewHydrator(eventManager, viewFactory, shadowDomStrategy); }); describe('hydrateDynamicComponentView', () => { @@ -111,6 +111,59 @@ export function main() { }); + describe('hydrateInPlaceHostView', () => { + + function createInPlaceHostView() { + var hostPv = createHostProtoView(createProtoView()); + var shadowView = createEmptyView(); + return createHostView(hostPv, shadowView); + } + + it('should hydrate the view', () => { + var hostView = createInPlaceHostView(); + viewHydrator.hydrateInPlaceHostView(null, hostView); + + expect(hostView.hydrated).toBe(true); + }); + + it('should store the view in the parent view', () => { + var parentView = createEmptyView(); + var hostView = createInPlaceHostView(); + + viewHydrator.hydrateInPlaceHostView(parentView, hostView); + + expect(parentView.imperativeHostViews).toEqual([hostView]); + + }); + + }); + + describe('dehydrateInPlaceHostView', () => { + + function createAndHydrateInPlaceHostView(parentView) { + var hostPv = createHostProtoView(createProtoView()); + var shadowView = createEmptyView(); + var hostView = createHostView(hostPv, shadowView); + viewHydrator.hydrateInPlaceHostView(parentView, hostView); + return hostView; + } + + it('should clear the host view', () => { + var parentView = createEmptyView(); + var hostView = createAndHydrateInPlaceHostView(parentView); + + var rootNodes = hostView.rootNodes; + expect(rootNodes[0].parentNode).toBeTruthy(); + + viewHydrator.dehydrateInPlaceHostView(parentView, hostView); + + expect(parentView.imperativeHostViews).toEqual([]); + expect(rootNodes[0].parentNode).toBeFalsy(); + expect(hostView.rootNodes).toEqual([]); + }); + + }); + describe('hydrate... shared functionality', () => { it('should hydrate existing child components', () => { @@ -128,9 +181,12 @@ export function main() { describe('dehydrate... shared functionality', () => { var hostView; - function createAndHydrate(nestedProtoView, shadowView) { + function createAndHydrate(nestedProtoView, shadowView, imperativeHostView = null) { var hostPv = createHostProtoView(nestedProtoView); hostView = createHostView(hostPv, shadowView); + if (isPresent(imperativeHostView)) { + viewHydrator.hydrateInPlaceHostView(hostView, imperativeHostView); + } hydrate(hostView); } @@ -152,15 +208,36 @@ export function main() { expect(hostView.componentChildViews[0]).toBe(shadowView); expect(shadowView.rootNodes[0].parentNode).toBeTruthy(); + expect(viewFactory.spy('returnView')).not.toHaveBeenCalled(); }); it('should clear dynamic child components', () => { var shadowView = createEmptyView(); createAndHydrate(null, shadowView); + expect(shadowView.rootNodes[0].parentNode).toBeTruthy(); + dehydrate(hostView); expect(hostView.componentChildViews[0]).toBe(null); expect(shadowView.rootNodes[0].parentNode).toBe(null); + expect(viewFactory.spy('returnView')).toHaveBeenCalledWith(shadowView); + }); + + it('should clear imperatively added child components', () => { + var shadowView = createEmptyView(); + createAndHydrate(createProtoView(), shadowView); + var impHostView = createHostView(createHostProtoView(createProtoView()), createEmptyView()); + shadowView.imperativeHostViews = [impHostView]; + + var rootNodes = impHostView.rootNodes; + expect(rootNodes[0].parentNode).toBeTruthy(); + + dehydrate(hostView); + + expect(shadowView.imperativeHostViews).toEqual([]); + expect(impHostView.rootNodes).toEqual([]); + expect(rootNodes[0].parentNode).toBeFalsy(); + expect(viewFactory.spy('returnView')).toHaveBeenCalledWith(impHostView); }); }); @@ -189,3 +266,9 @@ class SpyLightDom extends SpyObject { noSuchMethod(m){return super.noSuchMethod(m)} } +@proxy +@IMPLEMENTS(ViewFactory) +class SpyViewFactory extends SpyObject { + constructor(){super(ViewFactory);} + noSuchMethod(m){return super.noSuchMethod(m)} +}