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:
parent
02997f473a
commit
64ad74acbe
|
@ -34,7 +34,6 @@ export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy {
|
|||
}
|
||||
|
||||
attachTemplate(el, view:viewModule.RenderView) {
|
||||
DOM.clearNodes(el);
|
||||
moveViewNodesIntoParent(el, view);
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
];
|
||||
|
|
Loading…
Reference in New Issue