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:
parent
180e617866
commit
9d4111d69d
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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([
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user