-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e95afeb
Separate binding-specific code from template stamp. Expose override p…
kevinpschaaf 627352d
Updates based on PR feedback. API docs in progress.
kevinpschaaf eed6750
nodeInfo -> nodeInfoList
kevinpschaaf 1eb0df4
Add API docs.
kevinpschaaf b977480
[ci skip] Update doc
kevinpschaaf f87790d
[ci skip] Update doc
kevinpschaaf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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, ',').split(','); | ||
return parseArgs(args, sig); | ||
return parseArgs(args, sig, hostProps); | ||
} else { | ||
sig.args = emptyArray; | ||
return sig; | ||
|
@@ -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); | ||
|
@@ -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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove
!== null