Skip to content

Commit 9486f59

Browse files
author
Steve Orvell
committed
Merge pull request #2554 from Polymer/2182-kschaaf-string-interp
Adds compound binding. Fixes #2182
2 parents c00c772 + b2117dc commit 9486f59

File tree

8 files changed

+375
-125
lines changed

8 files changed

+375
-125
lines changed

PRIMER.md

+1-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<a name="feature-list"></a>
44
Below is a description of current Polymer features, followed by individual feature guides.
55

6-
See [the full Polymer.Base API documentation](http://polymer.github.io/polymer/) for details on specific methods and properties.
6+
See [the full Polymer.Base API documentation](http://polymer.github.io/polymer/) for details on specific methods and properties.
77

88
<a name="polymer-micro"></a>
99
**Basic Custom Element sugaring**
@@ -1085,17 +1085,10 @@ Polymer({
10851085

10861086
Properties of the custom element may be bound into text content or properties of local DOM elements using binding annotations in the template.
10871087

1088-
To bind to textContent, the binding annotation must currently span the entire content of the tag:
1089-
10901088
```html
10911089
<dom-module id="user-view">
10921090
<template>
10931091

1094-
<!-- Supported -->
1095-
First: <span>{{first}}</span><br>
1096-
Last: <span>{{last}}</span>
1097-
1098-
<!-- Not currently supported! -->
10991092
<div>First: {{first}}</div>
11001093
<div>Last: {{last}}</div>
11011094

src/lib/annotations/annotations.html

+104-55
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,91 @@
8888
this._parseElementAnnotations(node, list);
8989
},
9090

91-
_testEscape: function(value) {
92-
var escape = value.slice(0, 2);
93-
if (escape === '{{' || escape === '[[') {
94-
return escape;
91+
_bindingRegex: /([^{[]*)({{|\[\[)([^}\]]*)(?:]]|}})/g,
92+
93+
// TODO(kschaaf): We could modify this to allow an escape mechanism by
94+
// looking for the escape sequence in each of the matches and converting
95+
// the part back to a literal type, and then bailing if only literals
96+
// were found
97+
_parseBindings: function(text) {
98+
var re = this._bindingRegex;
99+
var parts = [];
100+
var m, lastIndex;
101+
// Example: "literal1{{binding1}}literal2[[binding2]]final"
102+
// Regex matches:
103+
// Iteration 1: Iteration 2:
104+
// m[1]: 'literal1' 'literal2'
105+
// m[2]: '{{' '[['
106+
// m[3]: 'binding1' 'binding2'
107+
// 'final' is manually substring'ed from end
108+
while ((m = re.exec(text)) !== null) {
109+
// Add literal part
110+
if (m[1]) {
111+
parts.push({literal: m[1]});
112+
}
113+
// Add binding part
114+
// Mode (one-way or two)
115+
var mode = m[2][0];
116+
var value = m[3].trim();
117+
// Negate
118+
var negate = false;
119+
if (value[0] == '!') {
120+
negate = true;
121+
value = value.substring(1).trim();
122+
}
123+
var customEvent, notifyEvent, colon;
124+
if (mode == '{' && (colon = value.indexOf('::')) > 0) {
125+
notifyEvent = value.substring(colon + 2);
126+
value = value.substring(0, colon);
127+
customEvent = true;
128+
}
129+
parts.push({
130+
compoundIndex: parts.length,
131+
value: value,
132+
mode: mode,
133+
negate: negate,
134+
event: notifyEvent,
135+
customEvent: customEvent
136+
});
137+
lastIndex = re.lastIndex;
138+
}
139+
// Add a final literal part
140+
if (lastIndex && lastIndex < text.length) {
141+
var literal = text.substring(lastIndex);
142+
if (literal) {
143+
parts.push({
144+
literal: literal
145+
});
146+
}
147+
}
148+
if (parts.length) {
149+
return parts;
95150
}
96151
},
97152

153+
_literalFromParts: function(parts) {
154+
var s = '';
155+
for (var i=0; i<parts.length; i++) {
156+
var literal = parts[i].literal;
157+
s += literal || '';
158+
}
159+
return s;
160+
},
161+
98162
// add annotations gleaned from TextNode `node` to `list`
99163
_parseTextNodeAnnotation: function(node, list) {
100-
var v = node.textContent;
101-
var escape = this._testEscape(v);
102-
if (escape) {
103-
// NOTE: use a space here so the textNode remains; some browsers
104-
// (IE) evacipate an empty textNode.
105-
node.textContent = ' ';
164+
var parts = this._parseBindings(node.textContent);
165+
if (parts) {
166+
// Initialize the textContent with any literal parts
167+
// NOTE: default to a space here so the textNode remains; some browsers
168+
// (IE) evacipate an empty textNode following cloneNode/importNode.
169+
node.textContent = this._literalFromParts(parts) || ' ';
106170
var annote = {
107171
bindings: [{
108172
kind: 'text',
109-
mode: escape[0],
110-
value: v.slice(2, -2).trim()
173+
name: 'textContent',
174+
parts: parts,
175+
isCompound: parts.length !== 1
111176
}]
112177
};
113178
list.push(annote);
@@ -152,7 +217,7 @@
152217
!node.hasAttribute('preserve-content')) {
153218
this._parseTemplate(node, i, list, annote);
154219
}
155-
// collapse adjacent textNodes: fixes an IE issue that can cause
220+
// collapse adjacent textNodes: fixes an IE issue that can cause
156221
// text nodes to be inexplicably split =(
157222
// note that root.normalize() should work but does not so we do this
158223
// manually.
@@ -203,67 +268,53 @@
203268
// `annotation data` is not as clear as it could be
204269
_parseNodeAttributeAnnotations: function(node, annotation) {
205270
for (var i=node.attributes.length-1, a; (a=node.attributes[i]); i--) {
206-
var n = a.name, v = a.value;
207-
// id (unless actually an escaped binding annotation)
208-
if (n === 'id' && !this._testEscape(v)) {
209-
annotation.id = v;
210-
}
271+
var n = a.name;
272+
var v = a.value;
273+
var b;
211274
// events (on-*)
212-
else if (n.slice(0, 3) === 'on-') {
275+
if (n.slice(0, 3) === 'on-') {
213276
node.removeAttribute(n);
214277
annotation.events.push({
215278
name: n.slice(3),
216279
value: v
217280
});
218281
}
219282
// bindings (other attributes)
220-
else {
221-
var b = this._parseNodeAttributeAnnotation(node, n, v);
222-
if (b) {
223-
annotation.bindings.push(b);
224-
}
283+
else if (b = this._parseNodeAttributeAnnotation(node, n, v)) {
284+
annotation.bindings.push(b);
285+
}
286+
// static id
287+
else if (n === 'id') {
288+
annotation.id = v;
225289
}
226290
}
227291
},
228292

229293
// construct annotation data from a generic attribute, or undefined
230-
_parseNodeAttributeAnnotation: function(node, n, v) {
231-
var escape = this._testEscape(v);
232-
if (escape) {
233-
var customEvent;
234-
// Cache name (`n` will be mangled)
235-
var name = n;
236-
// Mode (one-way or two)
237-
var mode = escape[0];
238-
v = v.slice(2, -2).trim();
239-
// Negate
240-
var not = false;
241-
if (v[0] == '!') {
242-
v = v.substring(1);
243-
not = true;
244-
}
294+
_parseNodeAttributeAnnotation: function(node, name, value) {
295+
var parts = this._parseBindings(value);
296+
if (parts) {
245297
// Attribute or property
298+
var origName = name;
246299
var kind = 'property';
247-
if (n[n.length-1] == '$') {
248-
name = n.slice(0, -1);
300+
if (name[name.length-1] == '$') {
301+
name = name.slice(0, -1);
249302
kind = 'attribute';
250303
}
251-
// Custom notification event
252-
var notifyEvent, colon;
253-
if (mode == '{' && (colon = v.indexOf('::')) > 0) {
254-
notifyEvent = v.substring(colon + 2);
255-
v = v.substring(0, colon);
256-
customEvent = true;
304+
// Initialize attribute bindings with any literal parts
305+
var literal = this._literalFromParts(parts);
306+
if (kind == 'attribute') {
307+
node.setAttribute(name, literal);
257308
}
258309
// Clear attribute before removing, since IE won't allow removing
259310
// `value` attribute if it previously had a value (can't
260311
// unconditionally set '' before removing since attributes with `$`
261312
// can't be set using setAttribute)
262-
if (node.localName == 'input' && n == 'value') {
263-
node.setAttribute(n, '');
313+
if (node.localName == 'input' && name == 'value') {
314+
node.setAttribute(origName, '');
264315
}
265316
// Remove annotation
266-
node.removeAttribute(n);
317+
node.removeAttribute(origName);
267318
// Case hackery: attributes are lower-case, but bind targets
268319
// (properties) are case sensitive. Gambit is to map dash-case to
269320
// camel-case: `foo-bar` becomes `fooBar`.
@@ -273,12 +324,10 @@
273324
}
274325
return {
275326
kind: kind,
276-
mode: mode,
277327
name: name,
278-
value: v,
279-
negate: not,
280-
event: notifyEvent,
281-
customEvent: customEvent
328+
parts: parts,
329+
literal: literal,
330+
isCompound: parts.length !== 1
282331
};
283332
}
284333
},

src/lib/bind/effects.html

+8-7
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515
_shouldAddListener: function(effect) {
1616
return effect.name &&
17-
effect.mode === '{' &&
18-
!effect.negate &&
19-
effect.kind != 'attribute'
20-
;
17+
effect.kind != 'attribute' &&
18+
effect.kind != 'text' &&
19+
!effect.isCompound &&
20+
effect.parts[0].mode === '{' &&
21+
!effect.parts[0].negate;
2122
},
2223

2324
_annotationEffect: function(source, value, effect) {
@@ -30,7 +31,7 @@
3031
// are used, since the target element may not dirty check (e.g. <input>)
3132
if (!effect.customEvent ||
3233
this._nodes[effect.index][effect.name] !== calc) {
33-
return this._applyEffectValue(calc, effect);
34+
return this._applyEffectValue(effect, calc);
3435
}
3536
},
3637

@@ -77,7 +78,7 @@
7778
if (args) {
7879
var fn = this[effect.method];
7980
if (fn) {
80-
this.__setProperty(effect.property, fn.apply(this, args));
81+
this.__setProperty(effect.name, fn.apply(this, args));
8182
} else {
8283
this._warn(this._logf('_computeEffect', 'compute method `' +
8384
effect.method + '` not defined'));
@@ -95,7 +96,7 @@
9596
if (effect.negate) {
9697
computedvalue = !computedvalue;
9798
}
98-
this._applyEffectValue(computedvalue, effect);
99+
this._applyEffectValue(effect, computedvalue);
99100
}
100101
} else {
101102
computedHost._warn(computedHost._logf('_annotatedComputationEffect',

0 commit comments

Comments
 (0)