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) {
DOM.clearNodes(el);
moveViewNodesIntoParent(el, view);
}

View File

@ -37,10 +37,7 @@ export class LightDom {
}
redistribute() {
var tags = this.contentTags();
if (tags.length > 0) {
redistributeNodes(tags, this.expandedDomNodes());
}
redistributeNodes(this.contentTags(), this.expandedDomNodes());
}
contentTags(): List<Content> {
@ -122,16 +119,22 @@ function redistributeNodes(contents:List<Content>, nodes:List) {
for (var i = 0; i < contents.length; ++i) {
var content = contents[i];
var select = content.select;
var matchSelector = (n) => DOM.elementMatches(n, select);
// Empty selector is identical to <content/>
if (select.length === 0) {
content.insert(nodes);
content.insert(ListWrapper.clone(nodes));
ListWrapper.clear(nodes);
} else {
var matchSelector = (n) => DOM.elementMatches(n, select);
var matchingNodes = ListWrapper.filter(nodes, matchSelector);
content.insert(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', () => {
var host = el('<div><span>original content</span></div>');
var originalChild = DOM.childNodes(host)[0];
var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], []);
strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host);
expect(DOM.tagName(firstChild).toLowerCase()).toEqual('div');
expect(firstChild).toHaveText('view');
expect(host).toHaveText('view');
expect(DOM.childNodes(host)[0]).toBe(originalChild);
expect(DOM.childNodes(host)[1]).toBe(nodes);
});
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', () => {
var host = el('<div><span>original content</span></div>');
var originalChild = DOM.childNodes(host)[0];
var nodes = el('<div>view</div>');
var view = new RenderView(null, [nodes], [], [], []);
strategy.attachTemplate(host, view);
var firstChild = DOM.firstChild(host);
expect(DOM.tagName(firstChild).toLowerCase()).toEqual('div');
expect(firstChild).toHaveText('view');
expect(host).toHaveText('view');
expect(DOM.childNodes(host)[0]).toBe(originalChild);
expect(DOM.childNodes(host)[1]).toBe(nodes);
});
it('should rewrite style urls', () => {

View File

@ -84,7 +84,7 @@ class FakeContentTag {
}
insert(nodes){
this._nodes = ListWrapper.clone(nodes);
this._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(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;
function createRenderer({templates}) {
function createRenderer({templates, viewCacheCapacity}) {
testbed = new IntegrationTestbed({
shadowDomStrategy: strategyFactory(),
templates: ListWrapper.concat(templates, componentTemplates)
templates: ListWrapper.concat(templates, componentTemplates),
viewCacheCapacity: viewCacheCapacity
});
renderer = testbed.renderer;
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) => {
createRenderer({
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
//it("should redistribute when a class has been added or removed");
//it('should not lose focus', () => {
@ -318,6 +378,12 @@ var simple = new DirectiveMetadata({
type: DirectiveMetadata.COMPONENT_TYPE
});
var empty = new DirectiveMetadata({
selector: 'empty',
id: 'empty',
type: DirectiveMetadata.COMPONENT_TYPE
});
var dynamicComponent = new DirectiveMetadata({
selector: 'dynamic',
id: 'dynamic',
@ -372,12 +438,29 @@ var autoViewportDirective = new DirectiveMetadata({
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 = [
new ViewDefinition({
componentId: 'simple',
template: 'SIMPLE(<content></content>)',
directives: []
}),
new ViewDefinition({
componentId: 'empty',
template: '',
directives: []
}),
new ViewDefinition({
componentId: 'multiple-content-tags',
template: '(<content select=".left"></content>, <content></content>)',
@ -407,5 +490,15 @@ var componentTemplates = [
componentId: 'conditional-content',
template: '<div>(<div *auto="cond"><content select=".left"></content></div>, <content></content>)</div>',
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]
})
];