From 8499cf84c3898d68b4dd478ba22c5dcbd82b93fd Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 10 Apr 2015 16:57:26 -0700 Subject: [PATCH] fix(shadow_dom): redistribute light dom when a dynamic component is attached. Fixes #1077 Closes #1315 --- modules/angular2/src/render/dom/view/view.js | 3 + .../direct_dom_renderer_integration_spec.js | 14 ++-- .../test/render/dom/integration_testbed.js | 18 +++-- .../shadow_dom_emulation_integration_spec.js | 50 +++++++++--- .../test/render/dom/view/view_spec.js | 76 +++++++++++++++++++ 5 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 modules/angular2/test/render/dom/view/view_spec.js diff --git a/modules/angular2/src/render/dom/view/view.js b/modules/angular2/src/render/dom/view/view.js index 97dcb2567d..d45c075bcb 100644 --- a/modules/angular2/src/render/dom/view/view.js +++ b/modules/angular2/src/render/dom/view/view.js @@ -69,6 +69,9 @@ export class RenderView { this.componentChildViews[elementIndex] = childView; if (this._hydrated) { childView.hydrate(lightDom); + if (isPresent(lightDom)) { + lightDom.redistribute(); + } } } 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 d2f8c8d03d..67c09c65e8 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 @@ -21,7 +21,7 @@ import {IntegrationTestbed, LoggingEventDispatcher, FakeEvent} from './integrati export function main() { describe('DirectDomRenderer integration', () => { - var testbed, renderer, eventPlugin, compile, rootEl; + var testbed, renderer, eventPlugin, compileRoot, rootEl; beforeEach(() => { rootEl = el('
'); @@ -36,7 +36,7 @@ export function main() { }); renderer = testbed.renderer; eventPlugin = testbed.eventPlugin; - compile = (rootEl, componentId) => testbed.compile(rootEl, componentId); + compileRoot = (rootEl, componentId) => testbed.compileRoot(rootEl, componentId); } it('should create root views while using the given elements in place', inject([AsyncTestCompleter], (async) => { @@ -93,7 +93,7 @@ export function main() { directives: [] })] }); - compile(rootEl, 'someComponent').then( (rootProtoView) => { + compileRoot(rootEl, 'someComponent').then( (rootProtoView) => { var viewRefs = renderer.createView(rootProtoView.render); renderer.setText(viewRefs[1], 0, 'hello'); expect(rootEl).toHaveText('hello'); @@ -109,7 +109,7 @@ export function main() { directives: [] })] }); - compile(rootEl, 'someComponent').then( (rootProtoView) => { + compileRoot(rootEl, 'someComponent').then( (rootProtoView) => { var viewRefs = renderer.createView(rootProtoView.render); renderer.setElementProperty(viewRefs[1], 0, 'value', 'hello'); expect(DOM.childNodes(rootEl)[0].value).toEqual('hello'); @@ -125,7 +125,7 @@ export function main() { directives: [] })] }); - compile(rootEl, 'someComponent').then( (rootProtoView) => { + compileRoot(rootEl, 'someComponent').then( (rootProtoView) => { var viewRef = renderer.createView(rootProtoView.render)[1]; var vcProtoViewRef = rootProtoView.elementBinders[0] .nestedProtoView.elementBinders[0].nestedProtoView.render; @@ -151,7 +151,7 @@ export function main() { })], viewCacheCapacity: 2 }); - compile(rootEl, 'someComponent').then( (rootProtoView) => { + compileRoot(rootEl, 'someComponent').then( (rootProtoView) => { var vcProtoViewRef = rootProtoView.elementBinders[0] .nestedProtoView.elementBinders[0].nestedProtoView.render; @@ -176,7 +176,7 @@ export function main() { directives: [] })] }); - compile(rootEl, 'someComponent').then( (rootProtoView) => { + compileRoot(rootEl, 'someComponent').then( (rootProtoView) => { var viewRef = renderer.createView(rootProtoView.render)[1]; var dispatcher = new LoggingEventDispatcher(); renderer.setEventDispatcher(viewRef, dispatcher); diff --git a/modules/angular2/test/render/dom/integration_testbed.js b/modules/angular2/test/render/dom/integration_testbed.js index e16b42206c..117f966c97 100644 --- a/modules/angular2/test/render/dom/integration_testbed.js +++ b/modules/angular2/test/render/dom/integration_testbed.js @@ -48,7 +48,7 @@ export class IntegrationTestbed { this.renderer = new DirectDomRenderer(compiler, viewFactory, shadowDomStrategy); } - compile(rootEl, componentId):Promise { + compileRoot(rootEl, componentId):Promise { return this.renderer.createRootProtoView(rootEl, componentId).then( (rootProtoView) => { return this._compileNestedProtoViews(rootProtoView, [ new DirectiveMetadata({ @@ -59,9 +59,13 @@ export class IntegrationTestbed { }); } - _compile(template):Promise { - return this.renderer.compile(template).then( (protoView) => { - return this._compileNestedProtoViews(protoView, template.directives); + compile(componentId):Promise { + var childTemplate = MapWrapper.get(this._templates, componentId); + if (isBlank(childTemplate)) { + throw new BaseException(`No template for component ${componentId}`); + } + return this.renderer.compile(childTemplate).then( (protoView) => { + return this._compileNestedProtoViews(protoView, childTemplate.directives); }); } @@ -80,9 +84,11 @@ export class IntegrationTestbed { if (isPresent(nestedComponentId)) { var childTemplate = MapWrapper.get(this._templates, nestedComponentId); if (isBlank(childTemplate)) { - throw new BaseException(`Could not find template for ${nestedComponentId}!`); + // dynamic component + ListWrapper.push(childComponentRenderPvRefs, null); + } else { + nestedCall = this.compile(nestedComponentId); } - nestedCall = this._compile(childTemplate); } else if (isPresent(elementBinder.nestedProtoView)) { nestedCall = this._compileNestedProtoViews(elementBinder.nestedProtoView, directives); } diff --git a/modules/angular2/test/render/dom/shadow_dom_emulation_integration_spec.js b/modules/angular2/test/render/dom/shadow_dom_emulation_integration_spec.js index 3dd712f86b..c4e7c299b2 100644 --- a/modules/angular2/test/render/dom/shadow_dom_emulation_integration_spec.js +++ b/modules/angular2/test/render/dom/shadow_dom_emulation_integration_spec.js @@ -52,7 +52,7 @@ export function main() { describe(`${name} shadow dom strategy`, () => { - var testbed, renderer, rootEl, compile; + var testbed, renderer, rootEl, compile, compileRoot; function createRenderer({templates}) { testbed = new IntegrationTestbed({ @@ -60,7 +60,8 @@ export function main() { templates: ListWrapper.concat(templates, componentTemplates) }); renderer = testbed.renderer; - compile = (rootEl, componentId) => testbed.compile(rootEl, componentId); + compileRoot = (rootEl, componentId) => testbed.compileRoot(rootEl, componentId); + compile = (componentId) => testbed.compile(componentId); } beforeEach( () => { @@ -77,7 +78,7 @@ export function main() { directives: [simple] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { renderer.createView(pv.render); expect(rootEl).toHaveText('SIMPLE(A)'); @@ -86,6 +87,29 @@ export function main() { }); })); + it('should support dynamic components', inject([AsyncTestCompleter], (async) => { + createRenderer({ + templates: [new ViewDefinition({ + componentId: 'main', + template: '' + + '
A
' + + '
', + directives: [dynamicComponent] + })] + }); + compileRoot(rootEl, 'main').then( (rootPv) => { + compile('simple').then( (simplePv) => { + var views = renderer.createView(rootPv.render); + var simpleViews = renderer.createView(simplePv.render); + renderer.setDynamicComponentView(views[1], 0, simpleViews[0]); + + expect(rootEl).toHaveText('SIMPLE(A)'); + + async.done(); + }); + }); + })); + it('should support multiple content tags', inject([AsyncTestCompleter], (async) => { createRenderer({ templates: [new ViewDefinition({ @@ -98,7 +122,7 @@ export function main() { directives: [multipleContentTagsComponent] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { renderer.createView(pv.render); expect(rootEl).toHaveText('(A, BC)'); @@ -118,7 +142,7 @@ export function main() { directives: [multipleContentTagsComponent] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { renderer.createView(pv.render); expect(rootEl).toHaveText('(, BAC)'); @@ -138,7 +162,7 @@ export function main() { directives: [multipleContentTagsComponent, manualViewportDirective] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { var viewRefs = renderer.createView(pv.render); var vcRef = new ViewContainerRef(viewRefs[1], 1); var vcProtoViewRef = pv.elementBinders[0].nestedProtoView @@ -170,7 +194,7 @@ export function main() { directives: [multipleContentTagsComponent, manualViewportDirective] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { var viewRefs = renderer.createView(pv.render); var vcRef = new ViewContainerRef(viewRefs[1], 1); var vcProtoViewRef = pv.elementBinders[0].nestedProtoView @@ -202,7 +226,7 @@ export function main() { directives: [outerWithIndirectNestedComponent] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { renderer.createView(pv.render); expect(rootEl).toHaveText('OUTER(SIMPLE(AB))'); @@ -223,7 +247,7 @@ export function main() { directives: [outerComponent, manualViewportDirective] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { var viewRefs = renderer.createView(pv.render); var vcRef = new ViewContainerRef(viewRefs[1], 1); var vcProtoViewRef = pv.elementBinders[0].nestedProtoView @@ -251,7 +275,7 @@ export function main() { directives: [conditionalContentComponent] })] }); - compile(rootEl, 'main').then( (pv) => { + compileRoot(rootEl, 'main').then( (pv) => { var viewRefs = renderer.createView(pv.render); var vcRef = new ViewContainerRef(viewRefs[2], 0); var vcProtoViewRef = pv.elementBinders[0].nestedProtoView @@ -302,6 +326,12 @@ var simple = new DirectiveMetadata({ type: DirectiveMetadata.COMPONENT_TYPE }); +var dynamicComponent = new DirectiveMetadata({ + selector: 'dynamic', + id: 'dynamic', + type: DirectiveMetadata.COMPONENT_TYPE +}); + var multipleContentTagsComponent = new DirectiveMetadata({ selector: 'multiple-content-tags', id: 'multiple-content-tags', diff --git a/modules/angular2/test/render/dom/view/view_spec.js b/modules/angular2/test/render/dom/view/view_spec.js new file mode 100644 index 0000000000..7368f66f49 --- /dev/null +++ b/modules/angular2/test/render/dom/view/view_spec.js @@ -0,0 +1,76 @@ +import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib'; + +import {ListWrapper} from 'angular2/src/facade/collection'; + +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'; + +export function main() { + + function createView() { + var proto = null; + var rootNodes = [el('
')]; + var boundTextNodes = []; + var boundElements = [el('
')]; + var viewContainers = []; + var contentTags = []; + return new RenderView(proto, rootNodes, + boundTextNodes, boundElements, viewContainers, contentTags); + } + + function createShadowDomStrategy(log) { + return new FakeShadowDomStrategy(log); + } + + describe('RenderView', () => { + var log, strategy; + + beforeEach( () => { + log = []; + strategy = createShadowDomStrategy(log); + }); + + describe('setComponentView', () => { + + it('should redistribute when a component is added to a hydrated view', () => { + var hostView = createView(); + var childView = createView(); + hostView.hydrate(null); + hostView.setComponentView(strategy, 0, childView); + expect(log[0]).toEqual(['redistribute']); + }); + + 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([]); + }); + + }); + + }); +} + +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); + } +} + +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