fix(compiler): make text interpolation more robust

Allows to add or remove previous siblings of text
interpolations (e.g. by added `<script>` tags for
content reproduction, or by removed `<style>` tags).

Also calculates correctly whether an element is
empty.

Fixes #2591
This commit is contained in:
Tobias Bosch 2015-06-18 13:15:11 -07:00
parent 180e617866
commit 9d4111d69d
6 changed files with 95 additions and 25 deletions

View File

@ -26,7 +26,7 @@ export class TextInterpolationParser implements CompileStep {
var expr = this._parser.parseInterpolation(text, current.elementDescription); var expr = this._parser.parseInterpolation(text, current.elementDescription);
if (isPresent(expr)) { if (isPresent(expr)) {
DOM.setText(node, ' '); DOM.setText(node, ' ');
current.bindElement().bindText(i, expr); current.bindElement().bindText(node, expr);
} }
} }
} }

View File

@ -26,17 +26,14 @@ export class DomProtoView {
boundTextNodeCount: number; boundTextNodeCount: number;
rootNodeCount: number; rootNodeCount: number;
constructor({elementBinders, element, transitiveContentTagCount}) { constructor({elementBinders, element, transitiveContentTagCount, boundTextNodeCount}) {
this.element = element; this.element = element;
this.elementBinders = elementBinders; this.elementBinders = elementBinders;
this.transitiveContentTagCount = transitiveContentTagCount; this.transitiveContentTagCount = transitiveContentTagCount;
this.isTemplateElement = DOM.isTemplateElement(this.element); this.isTemplateElement = DOM.isTemplateElement(this.element);
this.rootBindingOffset = this.rootBindingOffset =
(isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0; (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0;
this.boundTextNodeCount = this.boundTextNodeCount = boundTextNodeCount;
ListWrapper.reduce(elementBinders, (prevCount: number, elementBinder: ElementBinder) =>
prevCount + elementBinder.textNodeIndices.length,
0);
this.rootNodeCount = this.rootNodeCount =
this.isTemplateElement ? DOM.childNodes(DOM.content(this.element)).length : 1; this.isTemplateElement ? DOM.childNodes(DOM.content(this.element)).length : 1;
} }

View File

@ -48,6 +48,7 @@ export class ProtoViewBuilder {
var apiElementBinders = []; var apiElementBinders = [];
var transitiveContentTagCount = 0; var transitiveContentTagCount = 0;
var boundTextNodeCount = 0;
ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => { ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => {
var propertySetters = new Map(); var propertySetters = new Map();
var hostActions = new Map(); var hostActions = new Map();
@ -103,9 +104,10 @@ export class ProtoViewBuilder {
textBindings: ebb.textBindings, textBindings: ebb.textBindings,
readAttributes: ebb.readAttributes readAttributes: ebb.readAttributes
})); }));
var elementIsEmpty = this._isEmptyElement(ebb.element); var childNodeInfo = this._analyzeChildNodes(ebb.element, ebb.textBindingNodes);
boundTextNodeCount += ebb.textBindingNodes.length;
renderElementBinders.push(new ElementBinder({ renderElementBinders.push(new ElementBinder({
textNodeIndices: ebb.textBindingIndices, textNodeIndices: childNodeInfo.boundTextNodeIndices,
contentTagSelector: ebb.contentTagSelector, contentTagSelector: ebb.contentTagSelector,
parentIndex: parentIndex, parentIndex: parentIndex,
distanceToParent: ebb.distanceToParent, distanceToParent: ebb.distanceToParent,
@ -117,14 +119,15 @@ export class ProtoViewBuilder {
globalEvents: ebb.eventBuilder.buildGlobalEvents(), globalEvents: ebb.eventBuilder.buildGlobalEvents(),
hostActions: hostActions, hostActions: hostActions,
propertySetters: propertySetters, propertySetters: propertySetters,
elementIsEmpty: elementIsEmpty elementIsEmpty: childNodeInfo.elementIsEmpty
})); }));
}); });
return new api.ProtoViewDto({ return new api.ProtoViewDto({
render: new DomProtoViewRef(new DomProtoView({ render: new DomProtoViewRef(new DomProtoView({
element: this.rootElement, element: this.rootElement,
elementBinders: renderElementBinders, elementBinders: renderElementBinders,
transitiveContentTagCount: transitiveContentTagCount transitiveContentTagCount: transitiveContentTagCount,
boundTextNodeCount: boundTextNodeCount
})), })),
type: this.type, type: this.type,
elementBinders: apiElementBinders, elementBinders: apiElementBinders,
@ -132,19 +135,34 @@ export class ProtoViewBuilder {
}); });
} }
_isEmptyElement(el) { // Note: We need to calculate the next node indices not until the compilation is complete,
var childNodes = DOM.childNodes(el); // as the compiler might change the order of elements.
private _analyzeChildNodes(parentElement: /*element*/ any,
boundTextNodes: List</*node*/ any>): _ChildNodesInfo {
var childNodes = DOM.childNodes(DOM.templateAwareRoot(parentElement));
var boundTextNodeIndices = [];
var indexInBoundTextNodes = 0;
var elementIsEmpty = true;
for (var i = 0; i < childNodes.length; i++) { for (var i = 0; i < childNodes.length; i++) {
var node = childNodes[i]; var node = childNodes[i];
if ((DOM.isTextNode(node) && DOM.getText(node).trim().length > 0) || if (indexInBoundTextNodes < boundTextNodes.length &&
(DOM.isElementNode(node))) { node === boundTextNodes[indexInBoundTextNodes]) {
return false; boundTextNodeIndices.push(i);
indexInBoundTextNodes++;
elementIsEmpty = false;
} else if ((DOM.isTextNode(node) && DOM.getText(node).trim().length > 0) ||
(DOM.isElementNode(node))) {
elementIsEmpty = false;
} }
} }
return true; return new _ChildNodesInfo(boundTextNodeIndices, elementIsEmpty);
} }
} }
class _ChildNodesInfo {
constructor(public boundTextNodeIndices: List<number>, public elementIsEmpty: boolean) {}
}
export class ElementBinderBuilder { export class ElementBinderBuilder {
parent: ElementBinderBuilder = null; parent: ElementBinderBuilder = null;
distanceToParent: number = 0; distanceToParent: number = 0;
@ -154,7 +172,7 @@ export class ElementBinderBuilder {
variableBindings: Map<string, string> = new Map(); variableBindings: Map<string, string> = new Map();
eventBindings: List<api.EventBinding> = []; eventBindings: List<api.EventBinding> = [];
eventBuilder: EventBuilder = new EventBuilder(); eventBuilder: EventBuilder = new EventBuilder();
textBindingIndices: List<number> = []; textBindingNodes: List</*node*/ any> = [];
textBindings: List<ASTWithSource> = []; textBindings: List<ASTWithSource> = [];
contentTagSelector: string = null; contentTagSelector: string = null;
readAttributes: Map<string, string> = new Map(); readAttributes: Map<string, string> = new Map();
@ -214,8 +232,8 @@ export class ElementBinderBuilder {
this.eventBindings.push(this.eventBuilder.add(name, expression, target)); this.eventBindings.push(this.eventBuilder.add(name, expression, target));
} }
bindText(index, expression) { bindText(textNode, expression) {
this.textBindingIndices.push(index); this.textBindingNodes.push(textNode);
this.textBindings.push(expression); this.textBindings.push(expression);
} }

View File

@ -5,6 +5,7 @@ import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {Lexer, Parser} from 'angular2/change_detection'; import {Lexer, Parser} from 'angular2/change_detection';
import {IgnoreChildrenStep} from './pipeline_spec'; import {IgnoreChildrenStep} from './pipeline_spec';
import {ElementBinderBuilder} from 'angular2/src/render/dom/view/proto_view_builder'; import {ElementBinderBuilder} from 'angular2/src/render/dom/view/proto_view_builder';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() { export function main() {
describe('TextInterpolationParser', () => { describe('TextInterpolationParser', () => {
@ -20,7 +21,8 @@ export function main() {
function assertTextBinding(elementBinder, bindingIndex, nodeIndex, expression) { function assertTextBinding(elementBinder, bindingIndex, nodeIndex, expression) {
expect(elementBinder.textBindings[bindingIndex].source).toEqual(expression); expect(elementBinder.textBindings[bindingIndex].source).toEqual(expression);
expect(elementBinder.textBindingIndices[bindingIndex]).toEqual(nodeIndex); expect(elementBinder.textBindingNodes[bindingIndex])
.toEqual(DOM.childNodes(DOM.templateAwareRoot(elementBinder.element))[nodeIndex]);
} }
it('should find text interpolation in normal elements', () => { it('should find text interpolation in normal elements', () => {

View File

@ -36,15 +36,16 @@ import {DomTestbed} from './dom_testbed';
export function main() { export function main() {
describe('ShadowDom integration tests', function() { describe('ShadowDom integration tests', function() {
var styleHost;
var strategies = { var strategies = {
"scoped": "scoped":
bind(ShadowDomStrategy) bind(ShadowDomStrategy)
.toFactory((styleInliner, styleUrlResolver) => new EmulatedScopedShadowDomStrategy( .toFactory((styleInliner, styleUrlResolver) => new EmulatedScopedShadowDomStrategy(
styleInliner, styleUrlResolver, null), styleInliner, styleUrlResolver, styleHost),
[StyleInliner, StyleUrlResolver]), [StyleInliner, StyleUrlResolver]),
"unscoped": bind(ShadowDomStrategy) "unscoped": bind(ShadowDomStrategy)
.toFactory((styleUrlResolver) => .toFactory((styleUrlResolver) => new EmulatedUnscopedShadowDomStrategy(
new EmulatedUnscopedShadowDomStrategy(styleUrlResolver, null), styleUrlResolver, styleHost),
[StyleUrlResolver]) [StyleUrlResolver])
}; };
if (DOM.supportsNativeShadowDOM()) { if (DOM.supportsNativeShadowDOM()) {
@ -54,12 +55,15 @@ export function main() {
.toFactory((styleUrlResolver) => new NativeShadowDomStrategy(styleUrlResolver), .toFactory((styleUrlResolver) => new NativeShadowDomStrategy(styleUrlResolver),
[StyleUrlResolver])); [StyleUrlResolver]));
} }
beforeEach(() => { styleHost = el('<div></div>'); });
StringMapWrapper.forEach(strategies, (strategyBinding, name) => { StringMapWrapper.forEach(strategies, (strategyBinding, name) => {
describe(`${name} shadow dom strategy`, () => { describe(`${name} shadow dom strategy`, () => {
beforeEachBindings(() => { return [strategyBinding, DomTestbed]; }); beforeEachBindings(() => { return [strategyBinding, DomTestbed]; });
// GH-2095 - https://github.com/angular/angular/issues/2095 // GH-2095 - https://github.com/angular/angular/issues/2095
// important as we are adding a content end element during compilation,
// which could skrew up text node indices.
it('should support text nodes after content tags', it('should support text nodes after content tags',
inject([DomTestbed, AsyncTestCompleter], (tb, async) => { inject([DomTestbed, AsyncTestCompleter], (tb, async) => {
tb.compileAll([ tb.compileAll([
@ -80,6 +84,28 @@ export function main() {
}); });
})); }));
// important as we are moving style tags around during compilation,
// which could skrew up text node indices.
it('should support text nodes after style tags',
inject([DomTestbed, AsyncTestCompleter], (tb, async) => {
tb.compileAll([
simple,
new ViewDefinition({
componentId: 'simple',
template: '<style></style><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) => {
tb.compileAll([ tb.compileAll([
@ -102,6 +128,29 @@ export function main() {
}); });
})); }));
it('should support simple components with text interpolation as direct children',
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
tb.compileAll([
mainDir,
new ViewDefinition({
componentId: 'main',
template: '<simple>' +
'{{text}}' +
'</simple>',
directives: [simple]
}),
simpleTemplate
])
.then((protoViews) => {
var cmpView = tb.createRootViews(protoViews)[1];
tb.renderer.setText(cmpView.viewRef, 0, 'A');
expect(tb.rootEl).toHaveText('SIMPLE(A)');
async.done();
});
}));
it('should not show the light dom even if there is not content tag', it('should not show the light dom even if there is not content tag',
inject([AsyncTestCompleter, DomTestbed], (async, tb) => { inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
tb.compileAll([ tb.compileAll([

View File

@ -30,8 +30,12 @@ export function main() {
binders = []; binders = [];
} }
var rootEl = el('<div></div>'); var rootEl = el('<div></div>');
return new DomProtoView( return new DomProtoView({
{element: rootEl, elementBinders: binders, transitiveContentTagCount: 0}); element: rootEl,
elementBinders: binders,
transitiveContentTagCount: 0,
boundTextNodeCount: 0
});
} }
function createView(pv = null, boundElementCount = 0) { function createView(pv = null, boundElementCount = 0) {