Skip to content

Commit

Permalink
Separate binding-specific code from template stamp. Expose override p…
Browse files Browse the repository at this point in the history
…oints.
  • Loading branch information
kevinpschaaf committed Mar 15, 2017
1 parent a88766b commit e95afeb
Show file tree
Hide file tree
Showing 3 changed files with 372 additions and 462 deletions.
305 changes: 193 additions & 112 deletions lib/mixins/property-effects.html
Original file line number Diff line number Diff line change
Expand Up @@ -646,106 +646,6 @@
applyBindingValue(inst, info.methodInfo, val);
}

/**
* Post-processes template bindings (notes for short) provided by the
* Bindings library for use by the effects system:
* - Parses bindings for methods into method `signature` objects
* - Memoizes the root property for path bindings
* - Recurses into nested templates and processes those templates and
* extracts any host properties, which are set to the template's
* `_content._hostProps`
* - Adds bindings from the host to <template> elements for any nested
* template's lexically bound "host properties"; template handling
* elements can then add accessors to the template for these properties
* to forward host properties into template instances accordingly.
*
* @param {Array<Object>} notes List of notes to process; the notes are
* modified in place.
* @private
*/
function processAnnotations(notes) {
if (!notes._processed) {
for (let i=0; i<notes.length; i++) {
let note = notes[i];
// Parse bindings for methods & path roots (models)
for (let j=0; j<note.bindings.length; j++) {
let b = note.bindings[j];
for (let k=0; k<b.parts.length; k++) {
let p = b.parts[k];
if (!p.literal) {
p.signature = parseMethod(p.value);
if (!p.signature) {
p.rootProperty = Polymer.Path.root(p.value);
}
}
}
}
// Recurse into nested templates & bind host props
if (note.templateContent) {
processAnnotations(note.templateContent._notes);
let hostProps = note.templateContent._hostProps =
discoverTemplateHostProps(note.templateContent._notes);
let bindings = [];
for (let prop in hostProps) {
bindings.push({
index: note.index,
kind: 'property',
name: '_host_' + prop,
parts: [{
mode: '{',
value: prop
}]
});
}
note.bindings = note.bindings.concat(bindings);
}
}
notes._processed = true;
}
}

/**
* Finds all property usage in templates (property/path bindings and function
* arguments) and returns the path roots as keys in a map. Each outer template
* merges inner _hostProps to propagate inner host property needs to outer
* templates.
*
* @param {Array<Object>} notes List of notes to process for a given template
* @return {Object<string,boolean>} Map of host properties that the template
* (or any nested templates) uses
* @private
*/
function discoverTemplateHostProps(notes) {
let hostProps = {};
for (let i=0, n; (i<notes.length) && (n=notes[i]); i++) {
// Find all bindings to parent.* and spread them into _parentPropChain
for (let j=0, b$=n.bindings, b; (j<b$.length) && (b=b$[j]); j++) {
for (let k=0, p$=b.parts, p; (k<p$.length) && (p=p$[k]); k++) {
if (p.signature) {
let args = p.signature.args;
for (let kk=0; kk<args.length; kk++) {
let rootProperty = args[kk].rootProperty;
if (rootProperty) {
hostProps[rootProperty] = true;
}
}
hostProps[p.signature.methodName] = true;
} else {
if (p.rootProperty) {
hostProps[p.rootProperty] = true;
}
}
}
}
// Merge child _hostProps into this _hostProps
if (n.templateContent) {
let templateHostProps = n.templateContent._hostProps;
Object.assign(hostProps, templateHostProps);
}
}
return hostProps;
}

/**
* Returns true if a binding's metadata meets all the requirements to allow
* 2-way binding, and therefore a <property>-changed event listener should be
Expand Down Expand Up @@ -905,6 +805,91 @@

const emptyArray = [];

// Regular expressions used for binding
const IDENT = '(?:' + '[a-zA-Z_$][\\w.:$\\-*]*' + ')';
const NUMBER = '(?:' + '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?' + ')';
const SQUOTE_STRING = '(?:' + '\'(?:[^\'\\\\]|\\\\.)*\'' + ')';
const DQUOTE_STRING = '(?:' + '"(?:[^"\\\\]|\\\\.)*"' + ')';
const STRING = '(?:' + SQUOTE_STRING + '|' + DQUOTE_STRING + ')';
const ARGUMENT = '(?:' + IDENT + '|' + NUMBER + '|' + STRING + '\\s*' + ')';
const ARGUMENTS = '(?:' + ARGUMENT + '(?:,\\s*' + ARGUMENT + ')*' + ')';
const ARGUMENT_LIST = '(?:' + '\\(\\s*' +
'(?:' + ARGUMENTS + '?' + ')' +
'\\)\\s*' + ')';
const BINDING = '(' + IDENT + '\\s*' + ARGUMENT_LIST + '?' + ')'; // Group 3
const OPEN_BRACKET = '(\\[\\[|{{)' + '\\s*';
const CLOSE_BRACKET = '(?:]]|}})';
const NEGATE = '(?:(!)\\s*)?'; // Group 2
const EXPRESSION = OPEN_BRACKET + NEGATE + BINDING + CLOSE_BRACKET;
const bindingRegex = new RegExp(EXPRESSION, "g");

function parseBindings(text, hostProps) {
let parts = [];
let lastIndex = 0;
let m;
// Example: "literal1{{prop}}literal2[[!compute(foo,bar)]]final"
// Regex matches:
// Iteration 1: Iteration 2:
// m[1]: '{{' '[['
// m[2]: '' '!'
// m[3]: 'prop' 'compute(foo,bar)'
while ((m = bindingRegex.exec(text)) !== null) {
// Add literal part
if (m.index > lastIndex) {
parts.push({literal: text.slice(lastIndex, m.index)});
}
// Add binding part
// Mode (one-way or two)
let mode = m[1][0];
let negate = Boolean(m[2]);
let value = m[3].trim();
let customEvent, notifyEvent, colon;
if (mode == '{' && (colon = value.indexOf('::')) > 0) {
notifyEvent = value.substring(colon + 2);
value = value.substring(0, colon);
customEvent = true;
}
let signature = parseMethod(value, hostProps);
let rootProperty;
if (!signature) {
rootProperty = Polymer.Path.root(value);
hostProps[rootProperty] = true;
}
parts.push({
compoundIndex: parts.length,
value,
mode,
negate,
event: notifyEvent,
customEvent,
signature,
rootProperty
});
lastIndex = bindingRegex.lastIndex;
}
// Add a final literal part
if (lastIndex && lastIndex < text.length) {
let literal = text.substring(lastIndex);
if (literal) {
parts.push({
literal: literal
});
}
}
if (parts.length) {
return parts;
}
}

function literalFromParts(parts) {
let s = '';
for (let i=0; i<parts.length; i++) {
let literal = parts[i].literal;
s += literal || '';
}
return s;
}

/**
* Parses an expression string for a method signature, and returns a metadata
* describing the method in terms of `methodName`, `static` (whether all the
Expand All @@ -915,15 +900,18 @@
* found, otherwise `undefined`
* @private
*/
function parseMethod(expression) {
function parseMethod(expression, hostProps) {
// tries to match valid javascript property names
let m = expression.match(/([^\s]+?)\(([\s\S]*)\)/);
if (m) {
let sig = { methodName: m[1], static: true };
if (hostProps) {
hostProps[sig.methodName] = true;
}
if (m[2].trim()) {
// replace escaped commas with comma entity, split on un-escaped commas
let args = m[2].replace(/\\,/g, '&comma;').split(',');
return parseArgs(args, sig);
return parseArgs(args, sig, hostProps);
} else {
sig.args = emptyArray;
return sig;
Expand All @@ -942,11 +930,13 @@
* @return {Object} The updated signature metadata object
* @private
*/
function parseArgs(argList, sig) {
function parseArgs(argList, sig, hostProps) {
sig.args = argList.map(function(rawArg) {
let arg = parseArg(rawArg);
if (!arg.literal) {
sig.static = false;
} else if (hostProps) {
hostProps[arg.rootProperty] = true;
}
return arg;
}, this);
Expand Down Expand Up @@ -2178,23 +2168,114 @@
// Clear any existing propagation effects inherited from superClass
this.__propagateEffects = {};
this.__notifyListeners = [];
let notes = this._parseTemplateAnnotations(template);
processAnnotations(notes);
let notes = this.constructor._prepareTemplate(template);
for (let i=0, note; (i<notes.length) && (note=notes[i]); i++) {
// where to find the node in the concretized list
let b$ = note.bindings;
for (let j=0, binding; (j<b$.length) && (binding=b$[j]); j++) {
if (shouldAddListener(binding)) {
addAnnotatedListener(this, i, binding.name,
binding.parts[0].value,
binding.parts[0].event,
binding.parts[0].negate);
if (b$){
for (let j=0, binding; (j<b$.length) && (binding=b$[j]); j++) {
if (shouldAddListener(binding)) {
addAnnotatedListener(this, i, binding.name,
binding.parts[0].value,
binding.parts[0].event,
binding.parts[0].negate);
}
addBindingEffect(this, binding, i, dynamicFns);
}
addBindingEffect(this, binding, i, dynamicFns);
}
}
}

static _parseTemplateNode(node, note) {
let hostProps = note.notes.hostProps = note.notes.hostProps || {};
let noted = super._parseTemplateNode(node, note);
if (node.nodeType === Node.TEXT_NODE) {
let parts = parseBindings(node.textContent, hostProps);
if (parts) {
// Initialize the textContent with any literal parts
// NOTE: default to a space here so the textNode remains; some browsers
// (IE) evacipate an empty textNode following cloneNode/importNode.
node.textContent = literalFromParts(parts) || ' ';
note.bindings = note.bindings || [];
note.bindings.push({
kind: 'text',
name: 'textContent',
parts: parts,
isCompound: parts.length !== 1
});
noted = true;
}
}
return noted;
}

static _parseTemplateNodeAttribute(node, note, name, value) {
let parts = parseBindings(value, note.notes.hostProps);
if (parts) {
// Attribute or property
let origName = name;
let kind = 'property';
if (name[name.length-1] == '$') {
name = name.slice(0, -1);
kind = 'attribute';
}
// Initialize attribute bindings with any literal parts
let literal = literalFromParts(parts);
if (literal && kind == 'attribute') {
node.setAttribute(name, literal);
}
// Clear attribute before removing, since IE won't allow removing
// `value` attribute if it previously had a value (can't
// unconditionally set '' before removing since attributes with `$`
// can't be set using setAttribute)
if (node.localName === 'input' && origName === 'value') {
node.setAttribute(origName, '');
}
// Remove annotation
node.removeAttribute(origName);
// Case hackery: attributes are lower-case, but bind targets
// (properties) are case sensitive. Gambit is to map dash-case to
// camel-case: `foo-bar` becomes `fooBar`.
// Attribute bindings are excepted.
let propertyName = Polymer.CaseMap.dashToCamelCase(name);
if (kind === 'property') {
name = propertyName;
}
note.bindings = note.bindings || [];
note.bindings.push({
kind: kind,
name: name,
propertyName: propertyName,
parts: parts,
literal: literal,
isCompound: parts.length !== 1
});
return true;
} else {
return super._parseTemplateNodeAttribute(node, note, name, value);
}
}

static _parseTemplate(node, note) {
super._parseTemplate(node, note);
// Merge host props into outer template and add bindings
let hostProps = Object.assign(note.notes.hostProps,
note.templateContent._notes.hostProps);
for (let prop in hostProps) {
note.bindings = note.bindings || [];
note.bindings.push({
index: note.index,
kind: 'property',
name: '_host_' + prop,
parts: [{
mode: '{',
value: prop
}]
});
}
return true;
}

}

return PropertyEffects;
Expand Down
Loading

0 comments on commit e95afeb

Please sign in to comment.