fix(shadowdom): remove unused nodes on redistribute

Previously, light dom nodes that were not used by any content tag
were not removed from a view on redistribute. This lead
to a bug when reusing a view from the view pool, as it
still contained stale reprojected nodes.

Fixes #1416
This commit is contained in:
Tobias Bosch 2015-04-17 20:37:23 -07:00
parent 02997f473a
commit 64ad74acbe
6 changed files with 135 additions and 18 deletions

View File

@ -34,7 +34,6 @@ export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy {
} }
attachTemplate(el, view:viewModule.RenderView) { attachTemplate(el, view:viewModule.RenderView) {
DOM.clearNodes(el);
moveViewNodesIntoParent(el, view); moveViewNodesIntoParent(el, view);
} }

View File

@ -37,10 +37,7 @@ export class LightDom {
} }
redistribute() { redistribute() {
var tags = this.contentTags(); redistributeNodes(this.contentTags(), this.expandedDomNodes());
if (tags.length > 0) {
redistributeNodes(tags, this.expandedDomNodes());
}
} }
contentTags(): List<Content> { contentTags(): List<Content> {
@ -122,16 +119,22 @@ function redistributeNodes(contents:List<Content>, nodes:List) {
for (var i = 0; i < contents.length; ++i) { for (var i = 0; i < contents.length; ++i) {
var content = contents[i]; var content = contents[i];
var select = content.select; var select = content.select;
var matchSelector = (n) => DOM.elementMatches(n, select);
// Empty selector is identical to <content/> // Empty selector is identical to <content/>
if (select.length === 0) { if (select.length === 0) {
content.insert(nodes); content.insert(ListWrapper.clone(nodes));
ListWrapper.clear(nodes); ListWrapper.clear(nodes);
} else { } else {
var matchSelector = (n) => DOM.elementMatches(n, select);
var matchingNodes = ListWrapper.filter(nodes, matchSelector); var matchingNodes = ListWrapper.filter(nodes, matchSelector);
content.insert(matchingNodes); content.insert(matchingNodes);
ListWrapper.removeAll(nodes, matchingNodes); ListWrapper.removeAll(nodes, matchingNodes);
} }
} }
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (isPresent(node.parentNode)) {
DOM.remove(nodes[i]);
}
}
} }

View File

@ -46,14 +46,13 @@ export function main() {
it('should attach the view nodes as child of the host element', () => { it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>'); var host = el('<div><span>original content</span></div>');
var originalChild = DOM.childNodes(host)[0];
var nodes = el('<div>view</div>'); var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], []); var view = new RenderView(null, [nodes], [], [], []);
strategy.attachTemplate(host, view); strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host); expect(DOM.childNodes(host)[0]).toBe(originalChild);
expect(DOM.tagName(firstChild).toLowerCase()).toEqual('div'); expect(DOM.childNodes(host)[1]).toBe(nodes);
expect(firstChild).toHaveText('view');
expect(host).toHaveText('view');
}); });
it('should rewrite style urls', () => { it('should rewrite style urls', () => {

View File

@ -41,14 +41,13 @@ export function main() {
it('should attach the view nodes as child of the host element', () => { it('should attach the view nodes as child of the host element', () => {
var host = el('<div><span>original content</span></div>'); var host = el('<div><span>original content</span></div>');
var originalChild = DOM.childNodes(host)[0];
var nodes = el('<div>view</div>'); var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], []); var view = new RenderView(null, [nodes], [], [], []);
strategy.attachTemplate(host, view); strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host); expect(DOM.childNodes(host)[0]).toBe(originalChild);
expect(DOM.tagName(firstChild).toLowerCase()).toEqual('div'); expect(DOM.childNodes(host)[1]).toBe(nodes);
expect(firstChild).toHaveText('view');
expect(host).toHaveText('view');
}); });
it('should rewrite style urls', () => { it('should rewrite style urls', () => {

View File

@ -84,7 +84,7 @@ class FakeContentTag {
} }
insert(nodes){ insert(nodes){
this._nodes = ListWrapper.clone(nodes); this._nodes = nodes;
} }
nodes() { nodes() {
@ -215,6 +215,30 @@ export function main() {
expect(toHtml(wildcard.nodes())).toEqual(["<a>1</a>", "<b>2</b>", "<a>3</a>"]); expect(toHtml(wildcard.nodes())).toEqual(["<a>1</a>", "<b>2</b>", "<a>3</a>"]);
expect(toHtml(contentB.nodes())).toEqual([]); expect(toHtml(contentB.nodes())).toEqual([]);
}); });
it("should remove all nodes if there are no content tags", () => {
var lightDomEl = el("<div><a>1</a><b>2</b><a>3</a></div>")
var lightDom = new LightDom(lightDomView, new FakeView([]), lightDomEl);
lightDom.redistribute();
expect(DOM.childNodes(lightDomEl).length).toBe(0);
});
it("should remove all not projected nodes", () => {
var lightDomEl = el("<div><a>1</a><b>2</b><a>3</a></div>");
var bNode = DOM.childNodes(lightDomEl)[1];
var lightDom = new LightDom(lightDomView, new FakeView([
new FakeContentTag(null, "a")
]), lightDomEl);
lightDom.redistribute();
expect(bNode.parentNode).toBe(null);
});
}); });
}); });
} }

View File

@ -54,10 +54,11 @@ export function main() {
var testbed, renderer, rootEl, compile, compileRoot; var testbed, renderer, rootEl, compile, compileRoot;
function createRenderer({templates}) { function createRenderer({templates, viewCacheCapacity}) {
testbed = new IntegrationTestbed({ testbed = new IntegrationTestbed({
shadowDomStrategy: strategyFactory(), shadowDomStrategy: strategyFactory(),
templates: ListWrapper.concat(templates, componentTemplates) templates: ListWrapper.concat(templates, componentTemplates),
viewCacheCapacity: viewCacheCapacity
}); });
renderer = testbed.renderer; renderer = testbed.renderer;
compileRoot = (rootEl) => testbed.compileRoot(rootEl); compileRoot = (rootEl) => testbed.compileRoot(rootEl);
@ -87,6 +88,25 @@ export function main() {
}); });
})); }));
it('should not show the light dom event if there is not content tag', inject([AsyncTestCompleter], (async) => {
createRenderer({
templates: [new ViewDefinition({
componentId: 'main',
template: '<empty>' +
'<div>A</div>' +
'</empty>',
directives: [empty]
})]
});
compileRoot('main').then( (pv) => {
renderer.createInPlaceHostView(null, rootEl, pv.render);
expect(rootEl).toHaveText('');
async.done();
});
}));
it('should support dynamic components', inject([AsyncTestCompleter], (async) => { it('should support dynamic components', inject([AsyncTestCompleter], (async) => {
createRenderer({ createRenderer({
templates: [new ViewDefinition({ templates: [new ViewDefinition({
@ -289,6 +309,46 @@ export function main() {
}); });
})); }));
it("should support tabs with view caching", inject([AsyncTestCompleter], (async) => {
createRenderer({
templates: [new ViewDefinition({
componentId: 'main',
template:
'(<tab><span>0</span></tab>'+
'<tab><span>1</span></tab>'+
'<tab><span>2</span></tab>)',
directives: [tabComponent]
})],
viewCacheCapacity: 5
});
compileRoot('main').then( (pv) => {
var viewRefs = renderer.createInPlaceHostView(null, rootEl, pv.render);
var vcRef0 = new ViewContainerRef(viewRefs[2], 0);
var vcRef1 = new ViewContainerRef(viewRefs[3], 0);
var vcRef2 = new ViewContainerRef(viewRefs[4], 0);
var mainPv = pv.elementBinders[0].nestedProtoView;
var pvRef = mainPv.elementBinders[0].nestedProtoView.elementBinders[0].nestedProtoView.render;
expect(rootEl).toHaveText('()');
renderer.createViewInContainer(vcRef0, 0, pvRef);
expect(rootEl).toHaveText('(TAB(0))');
renderer.destroyViewInContainer(vcRef0, 0);
renderer.createViewInContainer(vcRef1, 0, pvRef);
expect(rootEl).toHaveText('(TAB(1))');
renderer.destroyViewInContainer(vcRef1, 0);
renderer.createViewInContainer(vcRef2, 0, pvRef);
expect(rootEl).toHaveText('(TAB(2))');
async.done();
});
}));
//Implement once NgElement support changing a class //Implement once NgElement support changing a class
//it("should redistribute when a class has been added or removed"); //it("should redistribute when a class has been added or removed");
//it('should not lose focus', () => { //it('should not lose focus', () => {
@ -318,6 +378,12 @@ var simple = new DirectiveMetadata({
type: DirectiveMetadata.COMPONENT_TYPE type: DirectiveMetadata.COMPONENT_TYPE
}); });
var empty = new DirectiveMetadata({
selector: 'empty',
id: 'empty',
type: DirectiveMetadata.COMPONENT_TYPE
});
var dynamicComponent = new DirectiveMetadata({ var dynamicComponent = new DirectiveMetadata({
selector: 'dynamic', selector: 'dynamic',
id: 'dynamic', id: 'dynamic',
@ -372,12 +438,29 @@ var autoViewportDirective = new DirectiveMetadata({
type: DirectiveMetadata.VIEWPORT_TYPE type: DirectiveMetadata.VIEWPORT_TYPE
}); });
var tabGroupComponent = new DirectiveMetadata({
selector: 'tab-group',
id: 'tab-group',
type: DirectiveMetadata.COMPONENT_TYPE
});
var tabComponent = new DirectiveMetadata({
selector: 'tab',
id: 'tab',
type: DirectiveMetadata.COMPONENT_TYPE
});
var componentTemplates = [ var componentTemplates = [
new ViewDefinition({ new ViewDefinition({
componentId: 'simple', componentId: 'simple',
template: 'SIMPLE(<content></content>)', template: 'SIMPLE(<content></content>)',
directives: [] directives: []
}), }),
new ViewDefinition({
componentId: 'empty',
template: '',
directives: []
}),
new ViewDefinition({ new ViewDefinition({
componentId: 'multiple-content-tags', componentId: 'multiple-content-tags',
template: '(<content select=".left"></content>, <content></content>)', template: '(<content select=".left"></content>, <content></content>)',
@ -407,5 +490,15 @@ var componentTemplates = [
componentId: 'conditional-content', componentId: 'conditional-content',
template: '<div>(<div *auto="cond"><content select=".left"></content></div>, <content></content>)</div>', template: '<div>(<div *auto="cond"><content select=".left"></content></div>, <content></content>)</div>',
directives: [autoViewportDirective] directives: [autoViewportDirective]
}),
new ViewDefinition({
componentId: 'tab-group',
template: 'GROUP(<content></content>)',
directives: []
}),
new ViewDefinition({
componentId: 'tab',
template: '<div><div *auto="cond">TAB(<content></content>)</div></div>',
directives: [autoViewportDirective]
}) })
]; ];