From 213dabdcebceda685031e541cf9ec5067838fd1b Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 14 Apr 2015 18:01:44 -0700 Subject: [PATCH] fix(view): remove dynamic components when the parent view is dehydrated Also adds a bunch of unit tests for affected parts. Fixes #1201 --- .../src/change_detection/interfaces.js | 1 + .../core/compiler/dynamic_component_loader.js | 7 +- .../src/core/compiler/element_binder.js | 10 +- modules/angular2/src/core/compiler/view.js | 27 ++- .../src/core/compiler/view_factory.js | 8 +- .../src/render/dom/view/element_binder.js | 9 + modules/angular2/src/render/dom/view/view.js | 5 + .../src/render/dom/view/view_factory.js | 3 +- modules/angular2/src/test_lib/test_lib.dart | 9 +- .../compiler/dynamic_component_loader_spec.js | 105 ++++++++- .../test/core/compiler/integration_spec.js | 36 ++- .../test/core/compiler/view_factory_spec.js | 132 +++++++++-- .../angular2/test/core/compiler/view_spec.js | 213 ++++++++++++++++++ .../test/render/dom/view/view_factory_spec.js | 150 ++++++++++-- .../test/render/dom/view/view_spec.js | 213 ++++++++++++++---- .../angular2/test/test_lib/test_lib_spec.js | 2 +- 16 files changed, 813 insertions(+), 117 deletions(-) create mode 100644 modules/angular2/test/core/compiler/view_spec.js diff --git a/modules/angular2/src/change_detection/interfaces.js b/modules/angular2/src/change_detection/interfaces.js index 41938652db..0231ca764e 100644 --- a/modules/angular2/src/change_detection/interfaces.js +++ b/modules/angular2/src/change_detection/interfaces.js @@ -49,6 +49,7 @@ export class ChangeDetector { addChild(cd:ChangeDetector) {} addShadowDomChild(cd:ChangeDetector) {} removeChild(cd:ChangeDetector) {} + removeShadowDomChild(cd:ChangeDetector) {} remove() {} hydrate(context:any, locals:Locals, directives:any) {} dehydrate() {} diff --git a/modules/angular2/src/core/compiler/dynamic_component_loader.js b/modules/angular2/src/core/compiler/dynamic_component_loader.js index 9a719e0537..ef113fe0b6 100644 --- a/modules/angular2/src/core/compiler/dynamic_component_loader.js +++ b/modules/angular2/src/core/compiler/dynamic_component_loader.js @@ -43,14 +43,12 @@ export class ComponentRef { export class DynamicComponentLoader { _compiler:Compiler; _viewFactory:ViewFactory; - _renderer:Renderer; _directiveMetadataReader:DirectiveMetadataReader; constructor(compiler:Compiler, directiveMetadataReader:DirectiveMetadataReader, renderer:Renderer, viewFactory:ViewFactory) { this._compiler = compiler; this._directiveMetadataReader = directiveMetadataReader; - this._renderer = renderer; this._viewFactory = viewFactory } @@ -67,16 +65,13 @@ export class DynamicComponentLoader { var hostEi = location.elementInjector; var hostView = location.hostView; - return this._compiler.compile(type).then(componentProtoView => { var component = hostEi.dynamicallyCreateComponent(type, directiveMetadata.annotation, inj); var componentView = this._instantiateAndHydrateView(componentProtoView, injector, hostEi, component); //TODO(vsavkin): do not use component child views as we need to clear the dynamically created views //same problem exists on the render side - hostView.addComponentChildView(componentView); - - this._renderer.setDynamicComponentView(hostView.render, location.boundElementIndex, componentView.render); + hostView.setDynamicComponentChildView(location.boundElementIndex, componentView); // TODO(vsavkin): return a component ref that dehydrates the component view and removes it // from the component child views diff --git a/modules/angular2/src/core/compiler/element_binder.js b/modules/angular2/src/core/compiler/element_binder.js index 92230a494f..d644124355 100644 --- a/modules/angular2/src/core/compiler/element_binder.js +++ b/modules/angular2/src/core/compiler/element_binder.js @@ -1,4 +1,4 @@ -import {int, isBlank, BaseException} from 'angular2/src/facade/lang'; +import {int, isBlank, isPresent, BaseException} from 'angular2/src/facade/lang'; import * as eiModule from './element_injector'; import {DirectiveBinding} from './element_injector'; import {List, StringMap} from 'angular2/src/facade/collection'; @@ -32,4 +32,12 @@ export class ElementBinder { // updated later, so we are able to resolve cycles this.nestedProtoView = null; } + + hasStaticComponent() { + return isPresent(this.componentDirective) && isPresent(this.nestedProtoView); + } + + hasDynamicComponent() { + return isPresent(this.componentDirective) && isBlank(this.nestedProtoView); + } } diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index 16ba1f3d47..ce2e40158c 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -143,7 +143,6 @@ export class AppView { } var binders = this.proto.elementBinders; - var componentChildViewIndex = 0; for (var i = 0; i < binders.length; ++i) { var componentDirective = binders[i].componentDirective; var shadowDomAppInjector = null; @@ -176,8 +175,8 @@ export class AppView { } } - if (isPresent(binders[i].nestedProtoView) && isPresent(componentDirective)) { - renderComponentIndex = this.componentChildViews[componentChildViewIndex].internalHydrateRecurse( + if (binders[i].hasStaticComponent()) { + renderComponentIndex = this.componentChildViews[i].internalHydrateRecurse( renderComponentViewRefs, renderComponentIndex, shadowDomAppInjector, @@ -185,7 +184,6 @@ export class AppView { elementInjector.getComponent(), null ); - componentChildViewIndex++; } } this._hydrateChangeDetector(); @@ -198,7 +196,15 @@ export class AppView { // componentChildViews for (var i = 0; i < this.componentChildViews.length; i++) { - this.componentChildViews[i].internalDehydrateRecurse(); + var componentView = this.componentChildViews[i]; + if (isPresent(componentView)) { + componentView.internalDehydrateRecurse(); + var binder = this.proto.elementBinders[i]; + if (binder.hasDynamicComponent()) { + this.componentChildViews[i] = null; + this.changeDetector.removeShadowDomChild(componentView.changeDetector); + } + } } // elementInjectors @@ -255,9 +261,16 @@ export class AppView { return elementInjector.getDirectiveAtIndex(directive.directiveIndex); } - addComponentChildView(view:AppView) { - ListWrapper.push(this.componentChildViews, view); + setDynamicComponentChildView(boundElementIndex, view:AppView) { + if (!this.proto.elementBinders[boundElementIndex].hasDynamicComponent()) { + throw new BaseException(`There is no dynamic component directive at element ${boundElementIndex}`); + } + if (isPresent(this.componentChildViews[boundElementIndex])) { + throw new BaseException(`There already is a bound component at element ${boundElementIndex}`); + } + this.componentChildViews[boundElementIndex] = view; this.changeDetector.addShadowDomChild(view.changeDetector); + this.proto.renderer.setDynamicComponentView(this.render, boundElementIndex, view.render); } // implementation of EventDispatcher#dispatchEvent diff --git a/modules/angular2/src/core/compiler/view_factory.js b/modules/angular2/src/core/compiler/view_factory.js index cb01d6a984..1996111a9e 100644 --- a/modules/angular2/src/core/compiler/view_factory.js +++ b/modules/angular2/src/core/compiler/view_factory.js @@ -57,7 +57,7 @@ export class ViewFactory { var rootElementInjectors = []; var preBuiltObjects = ListWrapper.createFixedSize(binders.length); var viewContainers = ListWrapper.createFixedSize(binders.length); - var componentChildViews = []; + var componentChildViews = ListWrapper.createFixedSize(binders.length); for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) { var binder = binders[binderIdx]; @@ -78,13 +78,13 @@ export class ViewFactory { // componentChildViews var bindingPropagationConfig = null; - if (isPresent(binder.nestedProtoView) && isPresent(binder.componentDirective)) { + if (binder.hasStaticComponent()) { var childView = this._createView(binder.nestedProtoView); - changeDetector.addChild(childView.changeDetector); + changeDetector.addShadowDomChild(childView.changeDetector); bindingPropagationConfig = new BindingPropagationConfig(childView.changeDetector); - ListWrapper.push(componentChildViews, childView); + componentChildViews[binderIdx] = childView; } // viewContainers diff --git a/modules/angular2/src/render/dom/view/element_binder.js b/modules/angular2/src/render/dom/view/element_binder.js index 9c978f8f15..b14e945e87 100644 --- a/modules/angular2/src/render/dom/view/element_binder.js +++ b/modules/angular2/src/render/dom/view/element_binder.js @@ -1,3 +1,4 @@ +import {isBlank, isPresent} from 'angular2/src/facade/lang'; import {AST} from 'angular2/change_detection'; import {SetterFn} from 'angular2/src/reflection/types'; import {List, ListWrapper} from 'angular2/src/facade/collection'; @@ -38,6 +39,14 @@ export class ElementBinder { this.distanceToParent = distanceToParent; this.propertySetters = propertySetters; } + + hasStaticComponent() { + return isPresent(this.componentId) && isPresent(this.nestedProtoView); + } + + hasDynamicComponent() { + return isPresent(this.componentId) && isBlank(this.nestedProtoView); + } } export class Event { diff --git a/modules/angular2/src/render/dom/view/view.js b/modules/angular2/src/render/dom/view/view.js index a366e2d58d..b30c423083 100644 --- a/modules/angular2/src/render/dom/view/view.js +++ b/modules/angular2/src/render/dom/view/view.js @@ -165,6 +165,11 @@ export class RenderView { var cv = this.componentChildViews[i]; if (isPresent(cv)) { cv.dehydrate(); + if (this.proto.elementBinders[i].hasDynamicComponent()) { + ViewContainer.removeViewNodes(cv); + this.lightDoms[i] = null; + this.componentChildViews[i] = null; + } } } diff --git a/modules/angular2/src/render/dom/view/view_factory.js b/modules/angular2/src/render/dom/view/view_factory.js index e50129b1f6..77910dfb21 100644 --- a/modules/angular2/src/render/dom/view/view_factory.js +++ b/modules/angular2/src/render/dom/view/view_factory.js @@ -84,7 +84,6 @@ export class ViewFactory { } else { viewRootNodes = [rootElementClone]; } - var binders = protoView.elementBinders; var boundTextNodes = []; var boundElements = ListWrapper.createFixedSize(binders.length); @@ -133,7 +132,7 @@ export class ViewFactory { var element = boundElements[binderIdx]; // static child components - if (isPresent(binder.componentId) && isPresent(binder.nestedProtoView)) { + if (binder.hasStaticComponent()) { var childView = this._createView(binder.nestedProtoView); view.setComponentView(this._shadowDomStrategy, binderIdx, childView); } diff --git a/modules/angular2/src/test_lib/test_lib.dart b/modules/angular2/src/test_lib/test_lib.dart index be005c3a9c..c8eeba7c83 100644 --- a/modules/angular2/src/test_lib/test_lib.dart +++ b/modules/angular2/src/test_lib/test_lib.dart @@ -1,7 +1,7 @@ library test_lib.test_lib; import 'package:guinness/guinness.dart' as gns; -export 'package:guinness/guinness.dart' hide Expect, expect, NotExpect, beforeEach, it, iit, xit; +export 'package:guinness/guinness.dart' hide Expect, expect, NotExpect, beforeEach, it, iit, xit, SpyObject; import 'package:unittest/unittest.dart' hide expect; import 'dart:async'; @@ -149,6 +149,13 @@ xit(name, fn) { _it(gns.xit, name, fn); } +class SpyObject extends gns.SpyObject { + // Need to take an optional type as this is required by + // the JS SpyObject. + SpyObject([type = null]) { + } +} + String elementText(n) { hasNodes(n) { var children = DOM.childNodes(n); 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 95ca21ce1d..ea6583f4b3 100644 --- a/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js +++ b/modules/angular2/test/core/compiler/dynamic_component_loader_spec.js @@ -1,22 +1,59 @@ -import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib'; +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + beforeEachBindings, + it, + xit, + SpyObject, proxy +} from 'angular2/test_lib'; +import {IMPLEMENTS} from 'angular2/src/facade/lang'; +import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; +import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader'; -import {Decorator, Viewport} from 'angular2/src/core/annotations/annotations'; - -@Decorator({selector: 'someDecorator'}) -class SomeDecorator {} - -@Viewport({selector: 'someViewport'}) -class SomeViewport {} +import {Decorator, Viewport, Component} from 'angular2/src/core/annotations/annotations'; +import {ElementRef, ElementInjector, ProtoElementInjector, PreBuiltObjects} from 'angular2/src/core/compiler/element_injector'; +import {Compiler} from 'angular2/src/core/compiler/compiler'; +import {AppProtoView, AppView} from 'angular2/src/core/compiler/view'; +import {ViewFactory} from 'angular2/src/core/compiler/view_factory' +import {Renderer} from 'angular2/src/render/api'; export function main() { describe("DynamicComponentLoader", () => { + var compiler; + var viewFactory; + var directiveMetadataReader; + var renderer; var loader; - beforeEach(() => { - loader = new DynamicComponentLoader(null, new DirectiveMetadataReader(), null, null); + beforeEach( () => { + compiler = new SpyCompiler(); + viewFactory = new SpyViewFactory(); + renderer = new SpyRenderer(); + directiveMetadataReader = new DirectiveMetadataReader(); + loader = new DynamicComponentLoader(compiler, directiveMetadataReader, renderer, viewFactory);; }); + function createProtoView() { + return new AppProtoView(null, null, null); + } + + function createElementRef(view, boundElementIndex) { + var peli = new ProtoElementInjector(null, boundElementIndex, []); + var eli = new ElementInjector(peli, null); + var preBuiltObjects = new PreBuiltObjects(view, null, null, null); + eli.instantiateDirectives(null, null, null, preBuiltObjects); + return new ElementRef(eli); + } + describe("loadIntoExistingLocation", () => { describe('Load errors', () => { it('should throw when trying to load a decorator', () => { @@ -29,7 +66,55 @@ export function main() { .toThrowError("Could not load 'SomeViewport' because it is not a component."); }); }); + + it('should add the child view into the host view', inject([AsyncTestCompleter], (async) => { + var log = []; + var hostView = new SpyAppView(); + var childView = new SpyAppView(); + hostView.spy('setDynamicComponentChildView').andCallFake( (boundElementIndex, childView) => { + ListWrapper.push(log, ['setDynamicComponentChildView', boundElementIndex, childView]); + }); + childView.spy('hydrate').andCallFake( (appInjector, hostElementInjector, context, locals) => { + ListWrapper.push(log, 'hydrate'); + }); + compiler.spy('compile').andCallFake( (_) => PromiseWrapper.resolve(createProtoView())); + viewFactory.spy('getView').andCallFake( (_) => childView); + + var elementRef = createElementRef(hostView, 23); + loader.loadIntoExistingLocation(SomeComponent, elementRef).then( (componentRef) => { + expect(log[0]).toEqual('hydrate'); + expect(log[1]).toEqual(['setDynamicComponentChildView', 23, childView]); + async.done(); + }); + })); + }); }); } + +@Decorator({selector: 'someDecorator'}) +class SomeDecorator {} + +@Viewport({selector: 'someViewport'}) +class SomeViewport {} + +@Component({selector: 'someComponent'}) +class SomeComponent {} + + +@proxy +@IMPLEMENTS(Compiler) +class SpyCompiler extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} + +@proxy +@IMPLEMENTS(ViewFactory) +class SpyViewFactory extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} + +@proxy +@IMPLEMENTS(Renderer) +class SpyRenderer extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} + +@proxy +@IMPLEMENTS(AppView) +class SpyAppView extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}} diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 493ac409d4..c6c0020c8e 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -567,7 +567,7 @@ export function main() { async.done(); }); })); - + it('should support render global events from multiple directives', inject([TestBed, AsyncTestCompleter], (tb, async) => { tb.overrideView(MyComp, new View({ template: '
', @@ -636,6 +636,40 @@ export function main() { }); }); })); + + it('should allow to destroy and create them via viewport directives', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new View({ + template: '
', + directives: [DynamicComp, If] + })); + + tb.createView(MyComp).then((view) => { + view.context.ctxBoolProp = true; + view.detectChanges(); + var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic"); + dynamicComponent.done.then((_) => { + view.detectChanges(); + expect(view.rootNodes).toHaveText('hello'); + + view.context.ctxBoolProp = false; + view.detectChanges(); + + expect(view.rawView.viewContainers[0].length).toBe(0); + expect(view.rootNodes).toHaveText(''); + + view.context.ctxBoolProp = true; + view.detectChanges(); + + var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic"); + return dynamicComponent.done; + }).then((_) => { + view.detectChanges(); + expect(view.rootNodes).toHaveText('hello'); + async.done(); + }); + }); + })); }); it('should support static attributes', inject([TestBed, AsyncTestCompleter], (tb, async) => { diff --git a/modules/angular2/test/core/compiler/view_factory_spec.js b/modules/angular2/test/core/compiler/view_factory_spec.js index 8007f2472d..eef71f16c9 100644 --- a/modules/angular2/test/core/compiler/view_factory_spec.js +++ b/modules/angular2/test/core/compiler/view_factory_spec.js @@ -1,23 +1,71 @@ -import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib'; - +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + beforeEachBindings, + it, + xit, + SpyObject, proxy +} from 'angular2/test_lib'; +import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang'; import {ViewFactory} from 'angular2/src/core/compiler/view_factory'; import {AppProtoView, AppView} from 'angular2/src/core/compiler/view'; import {dynamicChangeDetection} from 'angular2/change_detection'; +import {DirectiveBinding, ElementInjector} 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 {ElementBinder} from 'angular2/src/core/compiler/element_binder'; +import {ChangeDetector, ProtoChangeDetector} from 'angular2/change_detection'; export function main() { - function createViewFactory({capacity}):ViewFactory { - return new ViewFactory(capacity); - } + describe('AppViewFactory', () => { + var reader; - function createPv() { - return new AppProtoView(null, - null, - dynamicChangeDetection.createProtoChangeDetector('dummy', null)); - } + beforeEach( () => { + reader = new DirectiveMetadataReader(); + }); + + function createViewFactory({capacity}):ViewFactory { + return new ViewFactory(capacity); + } + + function createProtoChangeDetector() { + var pcd = new SpyProtoChangeDetector(); + pcd.spy('instantiate').andCallFake( (dispatcher, bindingRecords, variableBindings, directiveRecords) => { + return new SpyChangeDetector(); + }); + return pcd; + } + + function createProtoView(binders=null) { + if (isBlank(binders)) { + binders = []; + } + var pv = new AppProtoView(null, null, createProtoChangeDetector()); + pv.elementBinders = binders; + return pv; + } + + function createDirectiveBinding(type) { + var meta = reader.read(type); + return DirectiveBinding.createFromType(meta.type, meta.annotation); + } + + function createComponentElBinder(binding, nestedProtoView = null) { + var binder = new ElementBinder(0, null, 0, null, binding, null); + binder.nestedProtoView = nestedProtoView; + return binder; + } - describe('RenderViewFactory', () => { it('should create views', () => { - var pv = createPv(); + var pv = createProtoView(); var vf = createViewFactory({ capacity: 1 }); @@ -27,8 +75,8 @@ export function main() { describe('caching', () => { it('should support multiple AppProtoViews', () => { - var pv1 = createPv(); - var pv2 = createPv(); + var pv1 = createProtoView(); + var pv2 = createProtoView(); var vf = createViewFactory({ capacity: 2 }); var view1 = vf.getView(pv1); var view2 = vf.getView(pv2); @@ -40,7 +88,7 @@ export function main() { }); it('should reuse the newest view that has been returned', () => { - var pv = createPv(); + var pv = createProtoView(); var vf = createViewFactory({ capacity: 2 }); var view1 = vf.getView(pv); var view2 = vf.getView(pv); @@ -51,7 +99,7 @@ export function main() { }); it('should not add views when the capacity has been reached', () => { - var pv = createPv(); + var pv = createProtoView(); var vf = createViewFactory({ capacity: 2 }); var view1 = vf.getView(pv); var view2 = vf.getView(pv); @@ -66,5 +114,57 @@ export function main() { }); + describe('child components', () => { + + var vf; + + beforeEach(() => { + vf = createViewFactory({capacity: 1}); + }); + + it('should create static child component views', () => { + var hostPv = createProtoView([ + createComponentElBinder( + createDirectiveBinding(SomeComponent), + createProtoView() + ) + ]); + var hostView = vf.getView(hostPv); + var shadowView = hostView.componentChildViews[0]; + expect(shadowView).toBeTruthy(); + expect(hostView.changeDetector.spy('addShadowDomChild')).toHaveBeenCalledWith(shadowView.changeDetector); + }); + + it('should not create dynamic child component views', () => { + var hostPv = createProtoView([ + createComponentElBinder( + createDirectiveBinding(SomeComponent), + null + ) + ]); + var hostView = vf.getView(hostPv); + var shadowView = hostView.componentChildViews[0]; + expect(shadowView).toBeFalsy(); + }); + + }); + }); } + +@Component({ selector: 'someComponent' }) +class SomeComponent {} + +@proxy +@IMPLEMENTS(ChangeDetector) +class SpyChangeDetector extends SpyObject { + constructor(){super(ChangeDetector);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(ProtoChangeDetector) +class SpyProtoChangeDetector extends SpyObject { + constructor(){super(ProtoChangeDetector);} + noSuchMethod(m){return super.noSuchMethod(m)} +} diff --git a/modules/angular2/test/core/compiler/view_spec.js b/modules/angular2/test/core/compiler/view_spec.js new file mode 100644 index 0000000000..08daf38b30 --- /dev/null +++ b/modules/angular2/test/core/compiler/view_spec.js @@ -0,0 +1,213 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + beforeEachBindings, + it, + xit, + SpyObject, proxy +} from 'angular2/test_lib'; +import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang'; +import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; + +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 {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; +import {Component} from 'angular2/src/core/annotations/annotations'; + +export function main() { + describe('AppView', () => { + var renderer; + var reader; + + beforeEach( () => { + renderer = new SpyRenderer(); + reader = new DirectiveMetadataReader(); + }); + + function createDirectiveBinding(type) { + var meta = reader.read(type); + return DirectiveBinding.createFromType(meta.type, meta.annotation); + } + + function createElementInjector() { + var res = new SpyElementInjector(); + res.spy('isExportingComponent').andCallFake( () => false ); + res.spy('isExportingElement').andCallFake( () => false ); + return res; + } + + function createEmptyElBinder() { + return new ElementBinder(0, null, 0, null, null, null); + } + + function createComponentElBinder(binding, nestedProtoView = null) { + var binder = new ElementBinder(0, null, 0, null, binding, null); + binder.nestedProtoView = nestedProtoView; + return binder; + } + + function createProtoView(binders = null) { + if (isBlank(binders)) { + binders = []; + } + var res = new AppProtoView(renderer, null, null); + res.elementBinders = binders; + return res; + } + + function createHostProtoView(nestedProtoView) { + return createProtoView([ + createComponentElBinder( + createDirectiveBinding(SomeComponent), + nestedProtoView + ) + ]); + } + + function createHostView(pv, shadowView, componentInstance) { + var view = new AppView(pv, MapWrapper.create()); + var changeDetector = new SpyChangeDetector(); + var eij = createElementInjector(); + eij.spy('getComponent').andCallFake( () => componentInstance ); + view.init(changeDetector, [eij], [eij], + [null], [null], [shadowView]); + return view; + } + + describe('setDynamicComponentChildView', () => { + + it('should not allow to use non component indices', () => { + var pv = createProtoView([createEmptyElBinder()]); + var view = createHostView(pv, null, null); + var shadowView = new FakeAppView(); + expect( + () => view.setDynamicComponentChildView(0, shadowView) + ).toThrowError('There is no dynamic component directive at element 0'); + }); + + it('should not allow to use static component indices', () => { + var pv = createHostProtoView(createProtoView()); + var view = createHostView(pv, null, null); + var shadowView = new FakeAppView(); + expect( + () => view.setDynamicComponentChildView(0, shadowView) + ).toThrowError('There is no dynamic component directive at element 0'); + }); + + it('should not allow to overwrite an existing component', () => { + var pv = createHostProtoView(null); + var shadowView = new FakeAppView(); + var view = createHostView(pv, null, null); + view.setDynamicComponentChildView(0, shadowView); + expect( + () => view.setDynamicComponentChildView(0, shadowView) + ).toThrowError('There already is a bound component at element 0'); + }); + + }); + + describe('hydrate', () => { + + it('should hydrate existing child components', () => { + var hostPv = createHostProtoView(createProtoView()); + var componentInstance = {}; + var shadowView = new FakeAppView(); + var hostView = createHostView(hostPv, shadowView, componentInstance); + renderer.spy('createView').andCallFake( (_) => { + return [new ViewRef(), new ViewRef()]; + }); + + hostView.hydrate(null, null, null, null); + + expect(shadowView.spy('hydrate')).not.toHaveBeenCalled(); + expect(shadowView.spy('internalHydrateRecurse')).toHaveBeenCalled(); + }); + + }); + + describe('dehydrate', () => { + var hostView; + var shadowView; + + function createAndHydrate(nestedProtoView) { + var componentInstance = {}; + shadowView = new FakeAppView(); + var hostPv = createHostProtoView(nestedProtoView); + hostView = createHostView(hostPv, shadowView, componentInstance); + renderer.spy('createView').andCallFake( (_) => { + return [new ViewRef(), new ViewRef()]; + }); + + hostView.hydrate(null, null, null, null); + } + + it('should dehydrate child components', () => { + createAndHydrate(createProtoView()); + hostView.dehydrate(); + + expect(shadowView.spy('dehydrate')).not.toHaveBeenCalled(); + expect(shadowView.spy('internalDehydrateRecurse')).toHaveBeenCalled(); + }); + + it('should not clear static child components', () => { + createAndHydrate(createProtoView()); + hostView.dehydrate(); + + expect(hostView.componentChildViews[0]).toBe(shadowView); + expect(hostView.changeDetector.spy('removeShadowDomChild')).not.toHaveBeenCalled(); + }); + + it('should clear dynamic child components', () => { + createAndHydrate(null); + hostView.dehydrate(); + + expect(hostView.componentChildViews[0]).toBe(null); + expect(hostView.changeDetector.spy('removeShadowDomChild')).toHaveBeenCalledWith(shadowView.changeDetector); + }); + + }); + + }); +} + +@Component({ selector: 'someComponent' }) +class SomeComponent {} + +@proxy +@IMPLEMENTS(Renderer) +class SpyRenderer extends SpyObject { + constructor(){super(Renderer);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(ChangeDetector) +class SpyChangeDetector extends SpyObject { + constructor(){super(ChangeDetector);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(ElementInjector) +class SpyElementInjector extends SpyObject { + constructor(){super(ElementInjector);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(AppView) +class FakeAppView extends SpyObject { + constructor(){super(AppView);} + noSuchMethod(m){return super.noSuchMethod(m)} +} diff --git a/modules/angular2/test/render/dom/view/view_factory_spec.js b/modules/angular2/test/render/dom/view/view_factory_spec.js index e03b489fd9..3be319fed6 100644 --- a/modules/angular2/test/render/dom/view/view_factory_spec.js +++ b/modules/angular2/test/render/dom/view/view_factory_spec.js @@ -1,25 +1,69 @@ -import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib'; - +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + beforeEachBindings, + it, + xit, + SpyObject, proxy +} from 'angular2/test_lib'; +import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang'; +import {ListWrapper} from 'angular2/src/facade/collection'; import {ViewFactory} from 'angular2/src/render/dom/view/view_factory'; import {RenderProtoView} from 'angular2/src/render/dom/view/proto_view'; import {RenderView} from 'angular2/src/render/dom/view/view'; +import {ElementBinder} from 'angular2/src/render/dom/view/element_binder'; +import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy'; +import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom' +import {EventManager} from 'angular2/src/render/dom/events/event_manager'; export function main() { - function createViewFactory({capacity}):ViewFactory { - return new ViewFactory(capacity, null, null); - } - - function createPv() { - return new RenderProtoView({ - element: el('
'), - isRootView: false, - elementBinders: [] - }); - } - describe('RenderViewFactory', () => { + var eventManager; + var shadowDomStrategy; + + function createViewFactory({capacity}):ViewFactory { + return new ViewFactory(capacity, eventManager, shadowDomStrategy); + } + + function createProtoView(rootEl=null, binders=null) { + if (isBlank(rootEl)) { + rootEl = el('
'); + } + if (isBlank(binders)) { + binders = []; + } + return new RenderProtoView({ + element: rootEl, + isRootView: false, + elementBinders: binders + }); + } + + function createComponentElBinder(componentId, nestedProtoView = null) { + var binder = new ElementBinder({ + componentId: componentId, + textNodeIndices: [] + }); + binder.nestedProtoView = nestedProtoView; + return binder; + } + + + beforeEach( () => { + eventManager = new SpyEventManager(); + shadowDomStrategy = new SpyShadowDomStrategy(); + }); + it('should create views', () => { - var pv = createPv(); + var pv = createProtoView(); var vf = createViewFactory({ capacity: 1 }); @@ -29,8 +73,8 @@ export function main() { describe('caching', () => { it('should support multiple RenderProtoViews', () => { - var pv1 = createPv(); - var pv2 = createPv(); + var pv1 = createProtoView(); + var pv2 = createProtoView(); var vf = createViewFactory({ capacity: 2 }); var view1 = vf.getView(pv1); var view2 = vf.getView(pv2); @@ -42,7 +86,7 @@ export function main() { }); it('should reuse the newest view that has been returned', () => { - var pv = createPv(); + var pv = createProtoView(); var vf = createViewFactory({ capacity: 2 }); var view1 = vf.getView(pv); var view2 = vf.getView(pv); @@ -53,7 +97,7 @@ export function main() { }); it('should not add views when the capacity has been reached', () => { - var pv = createPv(); + var pv = createProtoView(); var vf = createViewFactory({ capacity: 2 }); var view1 = vf.getView(pv); var view2 = vf.getView(pv); @@ -68,5 +112,73 @@ export function main() { }); + describe('child components', () => { + + var vf, log; + + beforeEach(() => { + vf = createViewFactory({capacity: 1}); + log = []; + shadowDomStrategy.spy('attachTemplate').andCallFake( (el, view) => { + ListWrapper.push(log, ['attachTemplate', el, view]); + }); + shadowDomStrategy.spy('constructLightDom').andCallFake( (lightDomView, shadowDomView, el) => { + ListWrapper.push(log, ['constructLightDom', lightDomView, shadowDomView, el]); + return new SpyLightDom(); + }); + }); + + it('should create static child component views', () => { + var hostPv = createProtoView(el('
'), [ + createComponentElBinder( + 'someComponent', + createProtoView() + ) + ]); + var hostView = vf.getView(hostPv); + var shadowView = hostView.componentChildViews[0]; + expect(shadowView).toBeTruthy(); + expect(hostView.lightDoms[0]).toBeTruthy(); + expect(log[0]).toEqual(['constructLightDom', hostView, shadowView, hostView.boundElements[0]]); + expect(log[1]).toEqual(['attachTemplate', hostView.boundElements[0], shadowView]); + }); + + it('should not create dynamic child component views', () => { + var hostPv = createProtoView(el('
'), [ + createComponentElBinder( + 'someComponent', + null + ) + ]); + var hostView = vf.getView(hostPv); + var shadowView = hostView.componentChildViews[0]; + expect(shadowView).toBeFalsy(); + expect(hostView.lightDoms[0]).toBeFalsy(); + expect(log).toEqual([]); + }); + + }); + }); } + +@proxy +@IMPLEMENTS(EventManager) +class SpyEventManager extends SpyObject { + constructor(){super(EventManager);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(ShadowDomStrategy) +class SpyShadowDomStrategy extends SpyObject { + constructor(){super(ShadowDomStrategy);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(LightDom) +class SpyLightDom extends SpyObject { + constructor(){super(LightDom);} + noSuchMethod(m){return super.noSuchMethod(m)} +} diff --git a/modules/angular2/test/render/dom/view/view_spec.js b/modules/angular2/test/render/dom/view/view_spec.js index e7e2399cc7..191eb11d0f 100644 --- a/modules/angular2/test/render/dom/view/view_spec.js +++ b/modules/angular2/test/render/dom/view/view_spec.js @@ -1,53 +1,161 @@ -import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib'; - -import {ListWrapper} from 'angular2/src/facade/collection'; +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + beforeEachBindings, + it, + xit, + SpyObject, proxy +} from 'angular2/test_lib'; +import {IMPLEMENTS, isBlank} 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'; import {RenderView} from 'angular2/src/render/dom/view/view'; import {ShadowDomStrategy} from 'angular2/src/render/dom/shadow_dom/shadow_dom_strategy'; import {LightDom} from 'angular2/src/render/dom/shadow_dom/light_dom'; +import {EventManager} from 'angular2/src/render/dom/events/event_manager'; +import {DOM} from 'angular2/src/dom/dom_adapter'; export function main() { - function createView() { - var proto = new RenderProtoView({element: el('
'), isRootView: false, elementBinders: []}); - var rootNodes = [el('
')]; - var boundTextNodes = []; - var boundElements = [el('
')]; - var viewContainers = []; - var contentTags = []; - var eventManager = null; - return new RenderView(proto, rootNodes, - boundTextNodes, boundElements, viewContainers, contentTags, eventManager); - } - - function createShadowDomStrategy(log) { - return new FakeShadowDomStrategy(log); - } - describe('RenderView', () => { - var log, strategy; + var shadowDomStrategy; + var eventManager; + + function createProtoView({rootEl, binders}={}) { + if (isBlank(rootEl)) { + rootEl = el('
'); + } + if (isBlank(binders)) { + binders = []; + } + return new RenderProtoView({ + element: rootEl, + isRootView: false, + elementBinders: binders + }); + } + + function createComponentElBinder(componentId, nestedProtoView = null) { + var binder = new ElementBinder({ + componentId: componentId, + textNodeIndices: [] + }); + binder.nestedProtoView = nestedProtoView; + return binder; + } + + function createHostProtoView(nestedProtoView) { + return createProtoView({ + binders: [ + createComponentElBinder( + 'someComponent', + nestedProtoView + ) + ] + }); + } + + function createEmptyView() { + var root = el('
'); + return new RenderView(createProtoView(), [DOM.childNodes(root)[0]], + [], [], [], [], eventManager); + } + + function createHostView(pv, shadowDomView) { + var view = new RenderView(pv, [el('
')], + [], [el('
')], [], [], eventManager); + view.setComponentView(shadowDomStrategy, 0, shadowDomView); + return view; + } beforeEach( () => { - log = []; - strategy = createShadowDomStrategy(log); + eventManager = new SpyEventManager(); + shadowDomStrategy = new SpyShadowDomStrategy(); + shadowDomStrategy.spy('constructLightDom').andCallFake( (lightDomView, shadowDomView, el) => { + return new SpyLightDom(); + }); }); describe('setComponentView', () => { it('should redistribute when a component is added to a hydrated view', () => { - var hostView = createView(); - var childView = createView(); + var shadowView = new SpyRenderView(); + var hostPv = createHostProtoView(createProtoView()); + var hostView = createHostView(hostPv, shadowView); hostView.hydrate(null); - hostView.setComponentView(strategy, 0, childView); - expect(log[0]).toEqual(['redistribute']); + hostView.setComponentView(shadowDomStrategy, 0, shadowView); + var lightDomSpy:SpyLightDom = hostView.lightDoms[0]; + expect(lightDomSpy.spy('redistribute')).toHaveBeenCalled(); }); it('should not redistribute when a component is added to a dehydrated view', () => { - var hostView = createView(); - var childView = createView(); - hostView.setComponentView(strategy, 0, childView); - expect(log).toEqual([]); + var shadowView = new SpyRenderView(); + var hostPv = createHostProtoView(createProtoView()); + var hostView = createHostView(hostPv, shadowView); + hostView.setComponentView(shadowDomStrategy, 0, shadowView); + var lightDomSpy:SpyLightDom = hostView.lightDoms[0]; + expect(lightDomSpy.spy('redistribute')).not.toHaveBeenCalled(); + }); + + }); + + describe('hydrate', () => { + + it('should hydrate existing child components', () => { + var hostPv = createHostProtoView(createProtoView()); + var shadowView = new SpyRenderView(); + var hostView = createHostView(hostPv, shadowView); + + hostView.hydrate(null); + + expect(shadowView.spy('hydrate')).toHaveBeenCalled(); + }); + + }); + + describe('dehydrate', () => { + var hostView; + + function createAndHydrate(nestedProtoView, shadowView) { + var hostPv = createHostProtoView(nestedProtoView); + hostView = createHostView(hostPv, shadowView); + + hostView.hydrate(null); + } + + it('should dehydrate child components', () => { + var shadowView = new SpyRenderView(); + createAndHydrate(createProtoView(), shadowView); + hostView.dehydrate(); + + expect(shadowView.spy('dehydrate')).toHaveBeenCalled(); + }); + + it('should not clear static child components', () => { + var shadowView = createEmptyView(); + createAndHydrate(createProtoView(), shadowView); + hostView.dehydrate(); + + expect(hostView.componentChildViews[0]).toBe(shadowView); + expect(shadowView.rootNodes[0].parentNode).toBeTruthy(); + }); + + it('should clear dynamic child components', () => { + var shadowView = createEmptyView(); + createAndHydrate(null, shadowView); + hostView.dehydrate(); + + expect(hostView.componentChildViews[0]).toBe(null); + expect(shadowView.rootNodes[0].parentNode).toBe(null); }); }); @@ -55,24 +163,31 @@ export function main() { }); } -class FakeShadowDomStrategy extends ShadowDomStrategy { - log; - constructor(log) { - super(); - this.log = log; - } - constructLightDom(lightDomView:RenderView, shadowDomView:RenderView, element): LightDom { - return new FakeLightDom(this.log, lightDomView, shadowDomView, element); - } +@proxy +@IMPLEMENTS(EventManager) +class SpyEventManager extends SpyObject { + constructor(){super(EventManager);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(ShadowDomStrategy) +class SpyShadowDomStrategy extends SpyObject { + constructor(){super(ShadowDomStrategy);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(LightDom) +class SpyLightDom extends SpyObject { + constructor(){super(LightDom);} + noSuchMethod(m){return super.noSuchMethod(m)} +} + +@proxy +@IMPLEMENTS(RenderView) +class SpyRenderView extends SpyObject { + constructor(){super(RenderView);} + noSuchMethod(m){return super.noSuchMethod(m)} } -class FakeLightDom extends LightDom { - log; - constructor(log, lightDomView:RenderView, shadowDomView:RenderView, element) { - super(lightDomView, shadowDomView, element); - this.log = log; - } - redistribute() { - ListWrapper.push(this.log, ['redistribute']); - } -} \ No newline at end of file diff --git a/modules/angular2/test/test_lib/test_lib_spec.js b/modules/angular2/test/test_lib/test_lib_spec.js index 9dfc8992d7..904a7c5d12 100644 --- a/modules/angular2/test/test_lib/test_lib_spec.js +++ b/modules/angular2/test/test_lib/test_lib_spec.js @@ -81,7 +81,7 @@ export function main() { }); it('should create spys for all methods', () => { - expect(spyObj.someFunc).toBeTruthy(); + expect(() => spyObj.someFunc()).not.toThrow(); }); it('should create a default spy that does not fail for numbers', () => {