fix(Compiler): fix text nodes after content tags

fixes #2095
This commit is contained in:
Victor Berchet 2015-06-14 19:49:47 +02:00
parent b2e6ad85ea
commit d599fd3434
4 changed files with 59 additions and 30 deletions

View File

@ -1,6 +1,6 @@
import * as ldModule from './light_dom'; import * as ldModule from './light_dom';
import {DOM} from 'angular2/src/dom/dom_adapter'; import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent} from 'angular2/src/facade/lang'; import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection'; import {List, ListWrapper} from 'angular2/src/facade/collection';
class ContentStrategy { class ContentStrategy {
@ -20,7 +20,6 @@ class RenderedContent extends ContentStrategy {
constructor(contentEl) { constructor(contentEl) {
super(); super();
this.beginScript = contentEl; this.beginScript = contentEl;
this.endScript = DOM.nextSibling(this.beginScript);
this.nodes = []; this.nodes = [];
} }
@ -28,15 +27,23 @@ class RenderedContent extends ContentStrategy {
// Previous content is removed. // Previous content is removed.
insert(nodes: List</*node*/ any>) { insert(nodes: List</*node*/ any>) {
this.nodes = nodes; this.nodes = nodes;
if (isBlank(this.endScript)) {
// On first invocation, we need to create the end marker
this.endScript = DOM.createScriptTag('type', 'ng/contentEnd');
DOM.insertAfter(this.beginScript, this.endScript);
} else {
// On subsequent invocations, only remove all the nodes between the start end end markers
this._removeNodes();
}
DOM.insertAllBefore(this.endScript, nodes); DOM.insertAllBefore(this.endScript, nodes);
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]);
} }
_removeNodesUntil(node) { _removeNodes() {
var p = DOM.parentElement(this.beginScript); for (var node = DOM.nextSibling(this.beginScript); node !== this.endScript;
for (var next = DOM.nextSibling(this.beginScript); next !== node; node = DOM.nextSibling(this.beginScript)) {
next = DOM.nextSibling(this.beginScript)) { DOM.remove(node);
DOM.removeChild(p, next);
} }
} }
} }

View File

@ -47,13 +47,15 @@ export class ShadowDomCompileStep implements CompileStep {
var selector = MapWrapper.get(attrs, 'select'); var selector = MapWrapper.get(attrs, 'select');
selector = isPresent(selector) ? selector : ''; selector = isPresent(selector) ? selector : '';
// The content tag should be replaced by a pair of marker tags (start & end).
// The end marker creation is delayed to keep the number of elements constant.
// Creating the end marker here would invalidate the parent's textNodeIndices for the subsequent
// text nodes
var contentStart = DOM.createScriptTag('type', 'ng/contentStart'); var contentStart = DOM.createScriptTag('type', 'ng/contentStart');
if (assertionsEnabled()) { if (assertionsEnabled()) {
DOM.setAttribute(contentStart, 'select', selector); DOM.setAttribute(contentStart, 'select', selector);
} }
var contentEnd = DOM.createScriptTag('type', 'ng/contentEnd');
DOM.insertBefore(current.element, contentStart); DOM.insertBefore(current.element, contentStart);
DOM.insertBefore(current.element, contentEnd);
DOM.remove(current.element); DOM.remove(current.element);
current.element = contentStart; current.element = contentStart;

View File

@ -13,7 +13,6 @@ import {DOM} from 'angular2/src/dom/dom_adapter';
import {Content} from 'angular2/src/render/dom/shadow_dom/content_tag'; import {Content} from 'angular2/src/render/dom/shadow_dom/content_tag';
var _scriptStart = `<script start=""></script>`; var _scriptStart = `<script start=""></script>`;
var _scriptEnd = `<script end=""></script>`;
export function main() { export function main() {
describe('Content', function() { describe('Content', function() {
@ -21,35 +20,32 @@ export function main() {
var content; var content;
beforeEach(() => { beforeEach(() => {
parent = el(`<div>${_scriptStart}${_scriptEnd}`); parent = el(`<div>${_scriptStart}</div>`);
content = DOM.firstChild(parent); let contentStartMarker = DOM.firstChild(parent);
content = new Content(contentStartMarker, '');
}); });
it("should insert the nodes", () => { it("should insert the nodes", () => {
var c = new Content(content, ''); content.init(null);
c.init(null); content.insert([el("<a>A</a>"), el("<b>B</b>")]);
c.insert([el("<a></a>"), el("<b></b>")])
expect(DOM.getInnerHTML(parent)) expect(parent).toHaveText('AB');
.toEqual(`${_scriptStart}<a></a><b></b>${_scriptEnd}`);
}); });
it("should remove the nodes from the previous insertion", () => { it("should remove the nodes from the previous insertion", () => {
var c = new Content(content, ''); content.init(null);
c.init(null); content.insert([el("<a>A</a>")]);
c.insert([el("<a></a>")]); content.insert([el("<b>B</b>")]);
c.insert([el("<b></b>")]);
expect(DOM.getInnerHTML(parent)).toEqual(`${_scriptStart}<b></b>${_scriptEnd}`); expect(parent).toHaveText('B');
}); });
it("should insert empty list", () => { it("should clear nodes on inserting an empty list", () => {
var c = new Content(content, ''); content.init(null);
c.init(null); content.insert([el("<a>A</a>")]);
c.insert([el("<a></a>")]); content.insert([]);
c.insert([]);
expect(DOM.getInnerHTML(parent)).toEqual(`${_scriptStart}${_scriptEnd}`); expect(parent).toHaveText('');
}); });
}); });
} }

View File

@ -10,7 +10,7 @@ import {
it, it,
xit, xit,
beforeEachBindings, beforeEachBindings,
SpyObject, SpyObject
} from 'angular2/test_lib'; } from 'angular2/test_lib';
import {bind} from 'angular2/di'; import {bind} from 'angular2/di';
@ -34,6 +34,10 @@ import {StyleInliner} from 'angular2/src/render/dom/shadow_dom/style_inliner';
import {DomTestbed} from './dom_testbed'; import {DomTestbed} from './dom_testbed';
import {Injectable} from 'angular2/di';
import {Component, View} from 'angular2/annotations';
export function main() { export function main() {
describe('ShadowDom integration tests', function() { describe('ShadowDom integration tests', function() {
var strategies = { var strategies = {
@ -60,6 +64,26 @@ export function main() {
beforeEachBindings(() => { return [strategyBinding, DomTestbed]; }); beforeEachBindings(() => { return [strategyBinding, DomTestbed]; });
describe(`${name} shadow dom strategy`, () => { describe(`${name} shadow dom strategy`, () => {
// GH-2095 - https://github.com/angular/angular/issues/2095
it('should support text nodes after content tags',
inject([DomTestbed, AsyncTestCompleter], (tb, async) => {
tb.compileAll([
simple,
new ViewDefinition({
componentId: 'simple',
template: '<content></content><p>P,</p>{{a}}',
directives: []
})
])
.then((protoViewDtos) => {
var rootView = tb.createRootView(protoViewDtos[0]);
var cmpView = tb.createComponentView(rootView.viewRef, 0, protoViewDtos[1]);
tb.renderer.setText(cmpView.viewRef, 0, 'text');
expect(tb.rootEl).toHaveText('P,text');
async.done();
});
}));
it('should support simple components', it('should support simple components',
inject([AsyncTestCompleter, DomTestbed], (async, tb) => { inject([AsyncTestCompleter, DomTestbed], (async, tb) => {