Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate binding-specific code from template stamp. Expose override points #4432

Merged
merged 6 commits into from
Apr 4, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove !== 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({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

es6-ify

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