From f8f58e1031b204d0a213b63027095f67e78d23eb Mon Sep 17 00:00:00 2001 From: John Messerly Date: Thu, 25 Jul 2013 11:31:36 -0700 Subject: [PATCH 01/15] Add support for empty if, similar to bind and repeat. --- src/template_element.js | 1 + tests/template_element.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/template_element.js b/src/template_element.js index 512bbe8..8a70629 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -991,6 +991,7 @@ if (isTemplateNode) { if (name === IF) { ifFound = true; + value = value || '{{}}'; // Accept 'naked' if. } else if (name === BIND || name === REPEAT) { bindFound = true; value = value || '{{}}'; // Accept 'naked' bind & repeat. diff --git a/tests/template_element.js b/tests/template_element.js index 7246acb..5bb6a23 100644 --- a/tests/template_element.js +++ b/tests/template_element.js @@ -162,6 +162,20 @@ suite('Template Element', function() { assert.strictEqual('foo', div.lastChild.textContent); }); + test('Template-Empty If', function() { + var div = createTestHtml( + ''); + var m = { value: 'foo' }; + recursivelySetTemplateModel(div, null); + Platform.performMicrotaskCheckpoint(); + assert.strictEqual(1, div.childNodes.length); + + recursivelySetTemplateModel(div, m); + Platform.performMicrotaskCheckpoint(); + assert.strictEqual(2, div.childNodes.length); + assert.strictEqual('foo', div.lastChild.textContent); + }); + test('Template Repeat If', function() { var div = createTestHtml( ''); From 362246479a18214c01f4b89d3dc1516078fa3535 Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 1 Aug 2013 11:51:09 -0700 Subject: [PATCH 02/15] remove createBinding --- src/template_element.js | 106 +++++++++++++++----------------------- third_party/ChangeSummary | 2 +- 2 files changed, 43 insertions(+), 65 deletions(-) diff --git a/src/template_element.js b/src/template_element.js index 512bbe8..021a3b9 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -131,33 +131,14 @@ } Node.prototype.bind = function(name, model, path) { - this.bindings = this.bindings || {}; - var binding = this.bindings[name]; - if (binding) - binding.close(); - - binding = this.createBinding(name, model, path); - this.bindings[name] = binding; - if (!binding) { - console.error('Unhandled binding to Node: ', this, name, model, path); - return; - } - - return binding; + console.error('Unhandled binding to Node: ', this, name, model, path); }; - // TODO(rafaelw): This isn't really the right design. If node.bind() is - // specified, there's no way to host objects to invoke a "virtual" - // createBinding on custom elements. - Node.prototype.createBinding = function() {}; - Node.prototype.unbind = function(name) { - if (!this.bindings) - return; + this.bindings = this.bindings || {}; var binding = this.bindings[name]; - if (!binding) - return; - binding.close(); + if (binding && typeof binding.close === 'function') + binding.close(); delete this.bindings[name]; }; @@ -217,11 +198,12 @@ } }; - Text.prototype.createBinding = function(name, model, path) { - if (name === 'textContent') - return new NodeBinding(this, 'data', model, path); + Text.prototype.bind = function(name, model, path) { + if (name !== 'textContent') + return Node.prototype.bind.call(this, name, model, path); - return Node.prototype.createBinding.call(this, name, model, path); + this.unbind(name); + return this.bindings[name] = new NodeBinding(this, 'data', model, path); } function AttributeBinding(element, attributeName, model, path) { @@ -250,8 +232,9 @@ } }); - Element.prototype.createBinding = function(name, model, path) { - return new AttributeBinding(this, name, model, path); + Element.prototype.bind = function(name, model, path) { + this.unbind(name); + return this.bindings[name] = new AttributeBinding(this, name, model, path); }; var checkboxEventType; @@ -381,29 +364,24 @@ } }); - HTMLInputElement.prototype.createBinding = function(name, model, path) { - if (name === 'value') { - // TODO(rafaelw): Maybe template should remove all binding instructions. - this.removeAttribute(name); - return new InputBinding(this, 'value', model, path) - } - - if (name === 'checked') { - this.removeAttribute(name); - return new CheckedBinding(this, model, path); - } + HTMLInputElement.prototype.bind = function(name, model, path) { + if (name !== 'value' && name !== 'checked') + return HTMLElement.prototype.bind.call(this, name, model, path); - return HTMLElement.prototype.createBinding.call(this, name, model, path); + this.unbind(name); + this.removeAttribute(name); + return this.bindings[name] = name === 'value' ? + new InputBinding(this, 'value', model, path) : + new CheckedBinding(this, model, path); } - HTMLTextAreaElement.prototype.createBinding = function(name, model, path) { - if (name === 'value') { - // TODO(rafaelw): Maybe template should remove all binding instructions. - this.removeAttribute(name); - return new InputBinding(this, name, model, path) - } + HTMLTextAreaElement.prototype.bind = function(name, model, path) { + if (name !== 'value') + return HTMLElement.prototype.bind.call(this, name, model, path); - return HTMLElement.prototype.createBinding.call(this, name, model, path); + this.unbind(name); + this.removeAttribute(name); + return this.bindings[name] = new InputBinding(this, name, model, path); } function SelectedIndexBinding(element, model, path) { @@ -435,14 +413,13 @@ } }); - HTMLSelectElement.prototype.createBinding = function(name, model, path) { - if (name.toLowerCase() === 'selectedindex') { - // TODO(rafaelw): Maybe template should remove all binding instructions. - this.removeAttribute(name); - return new SelectedIndexBinding(this, model, path); - } + HTMLSelectElement.prototype.bind = function(name, model, path) { + if (name.toLowerCase() !== 'selectedindex') + return HTMLElement.prototype.bind.call(this, name, model, path); - return HTMLElement.prototype.createBinding.call(this, name, model, path); + this.unbind(name); + this.removeAttribute(name); + return this.bindings[name] = new SelectedIndexBinding(this, model, path); } var BIND = 'bind'; @@ -809,18 +786,19 @@ }); mixin(HTMLTemplateElement.prototype, { - createBinding: function(name, model, path) { - if (name === BIND || name === REPEAT || name === IF) { - var iterator = templateIteratorTable.get(this); - if (!iterator) { - iterator = new TemplateIterator(this); - templateIteratorTable.set(this, iterator); - } + bind: function(name, model, path) { + if (name !== BIND && name !== REPEAT && name !== IF) + return HTMLElement.prototype.bind.call(this, name, model, path); - return new TemplateBinding(iterator, name, model, path || ''); + + var iterator = templateIteratorTable.get(this); + if (!iterator) { + iterator = new TemplateIterator(this); + templateIteratorTable.set(this, iterator); } - return HTMLElement.prototype.createBinding.call(this, name, model, path); + this.unbind(name); + return this.bindings[name] = new TemplateBinding(iterator, name, model, path || ''); }, createInstance: function(model, delegate, bound) { diff --git a/third_party/ChangeSummary b/third_party/ChangeSummary index df45ea3..a799821 160000 --- a/third_party/ChangeSummary +++ b/third_party/ChangeSummary @@ -1 +1 @@ -Subproject commit df45ea3e6469fa8ae0c07d82b5041f2aaffaeec7 +Subproject commit a799821f1138c06884dec90c685bd05501611e78 From a0c21e3e5b83311a24ec312dfde4ea90fe41e060 Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 1 Aug 2013 11:57:22 -0700 Subject: [PATCH 03/15] whitespace --- src/template_element.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/template_element.js b/src/template_element.js index 021a3b9..3c51abf 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -371,8 +371,8 @@ this.unbind(name); this.removeAttribute(name); return this.bindings[name] = name === 'value' ? - new InputBinding(this, 'value', model, path) : - new CheckedBinding(this, model, path); + new InputBinding(this, 'value', model, path) : + new CheckedBinding(this, model, path); } HTMLTextAreaElement.prototype.bind = function(name, model, path) { @@ -798,7 +798,8 @@ } this.unbind(name); - return this.bindings[name] = new TemplateBinding(iterator, name, model, path || ''); + return this.bindings[name] = + new TemplateBinding(iterator, name, model, path || ''); }, createInstance: function(model, delegate, bound) { From d1e1bcc3efa99390f736e5a6574d06588361e61a Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 1 Aug 2013 14:14:01 -0700 Subject: [PATCH 04/15] Remove TemplateCloser and better encapsulate templateIteratorTable R=arv BUG= Review URL: https://codereview.appspot.com/12286043 --- src/template_element.js | 45 +++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/template_element.js b/src/template_element.js index 3c51abf..d8c4088 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -791,12 +791,7 @@ return HTMLElement.prototype.bind.call(this, name, model, path); - var iterator = templateIteratorTable.get(this); - if (!iterator) { - iterator = new TemplateIterator(this); - templateIteratorTable.set(this, iterator); - } - + var iterator = TemplateIterator.getOrCreate(this); this.unbind(name); return this.bindings[name] = new TemplateBinding(iterator, name, model, path || ''); @@ -1004,18 +999,6 @@ } } - function TemplateCloser(node) { - this.node = node; - } - - TemplateCloser.prototype = { - close: function() { - var iterator = templateIteratorTable.get(this.node); - if (iterator) - iterator.close(); - } - } - function addMapBindings(node, bindings, model, delegate, bound) { if (!bindings) return; @@ -1025,9 +1008,6 @@ if (delegate) { templateBindingDelegateTable.set(node, delegate); } - if (bound) { - bound.push(new TemplateCloser(node)); - } } if (bindings.length) @@ -1215,8 +1195,20 @@ this.iteratedValue = undefined; this.arrayObserver = undefined; this.inputs = new CompoundBinding(this.resolveInputs.bind(this)); + TemplateIterator.templateTable.set(this.templateElement_, this); + } + + // "private" + TemplateIterator.templateTable = new SideTable(); + + TemplateIterator.get = function(template) { + return TemplateIterator.templateTable.get(template); } + TemplateIterator.getOrCreate = function(template) { + return TemplateIterator.get(template) || new TemplateIterator(template); + }; + TemplateIterator.prototype = { resolveInputs: function(values) { if (this.closed) @@ -1251,11 +1243,8 @@ if (splices.length) this.handleSplices(splices); - if (!this.inputs.size) { - // End iteration - templateIteratorTable.delete(this.templateElement_); + if (!this.inputs.size) this.close(); - } }, getTerminatorAt: function(index) { @@ -1267,7 +1256,7 @@ return terminator; } - var subIterator = templateIteratorTable.get(terminator); + var subIterator = TemplateIterator.get(terminator); if (!subIterator) return terminator; @@ -1329,7 +1318,6 @@ var template = this.templateElement_; if (!template.parentNode || !template.ownerDocument.defaultView) { this.close(); - templateIteratorTable.delete(this); return; } @@ -1398,12 +1386,11 @@ this.terminators.length = 0; this.inputs.close(); + TemplateIterator.templateTable.delete(this.templateElement_); this.closed = true; } }; - var templateIteratorTable = new SideTable(); - global.CompoundBinding = CompoundBinding; // Polyfill-specific API. From edd23f6770cea1669e1802fe0ac2f380f100ce7d Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 1 Aug 2013 15:34:16 -0700 Subject: [PATCH 05/15] extract method closeInstanceBindings for TemplateIterator R=arv BUG= Review URL: https://codereview.appspot.com/12254045 --- src/template_element.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/template_element.js b/src/template_element.js index d8c4088..65cbec0 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -1358,12 +1358,14 @@ }, this); instanceCache.forEach(function(instanceNodes) { - var bound = instanceNodes.bound; + this.closeInstanceBindings(instanceNodes.bound); + }, this); + }, - for (var i = 0; i < bound.length; i++) { - bound[i].close(); - } - }); + closeInstanceBindings: function(bound) { + for (var i = 0; i < bound.length; i++) { + bound[i].close(); + } }, unobserve: function() { @@ -1379,9 +1381,7 @@ return; this.unobserve(); for (var i = 1; i < this.terminators.length; i += 2) { - var bound = this.terminators[i]; - for (var j = 0; j < bound.length; j++) - bound[j].close(); + this.closeInstanceBindings(this.terminators[i]); } this.terminators.length = 0; From 32d409c09930e53d47256638f710a696dc39080f Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 1 Aug 2013 16:45:57 -0700 Subject: [PATCH 06/15] dont overload binding apis for template binding john points out that it's weird for TemplateBinding.node to point to the template iterator. We don't really have a clear delineation of public vs private API right now, but I agree that the iterator reference should really be private. I'm not prepared yet to go through and start marking private members with trailing underscores. We had that for a while and it just go really ugly. Hopefully, once we get all of our semantics nailed down, we can write the spec and the IDL and that will make it clear. r=arv BUG= Review URL: https://codereview.appspot.com/12296043 --- src/template_element.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/template_element.js b/src/template_element.js index 58bb74a..433d8fd 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -762,13 +762,14 @@ ensureScheduled(setModelFn); } - function TemplateBinding(node, property, model, path) { + function TemplateBinding(node, property, model, path, iterator) { this.closed = false; this.node = node; this.property = property; this.model = model; this.path = path; - this.node.inputs.bind(this.property, model, path || ''); + this.iterator = iterator; + this.iterator.inputs.bind(this.property, model, path || ''); } TemplateBinding.prototype = createObject({ @@ -778,7 +779,8 @@ close: function() { if (this.closed) return; - this.node.inputs.unbind(this.property); + this.iterator.inputs.unbind(this.property); + this.iterator = undefined; this.node = undefined; this.model = undefined; this.closed = true; @@ -794,7 +796,7 @@ var iterator = TemplateIterator.getOrCreate(this); this.unbind(name); return this.bindings[name] = - new TemplateBinding(iterator, name, model, path || ''); + new TemplateBinding(this, name, model, path || '', iterator); }, createInstance: function(model, delegate, bound) { From 05fe3fad7c282009c7ab5a5a0d9b13502a98c550 Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 1 Aug 2013 16:58:51 -0700 Subject: [PATCH 07/15] make path in bind(name, model, path) default to empty string I think if we spec'd Node.bind, path would be optional and default to '' (in WebIDL). I realize this impl is kind of cheating, but I think it's the best current option. R=arv BUG= Review URL: https://codereview.appspot.com/12299043 --- src/template_element.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/template_element.js b/src/template_element.js index 433d8fd..6542fac 100644 --- a/src/template_element.js +++ b/src/template_element.js @@ -160,9 +160,10 @@ this.node = node; this.property = property; this.model = model; - this.path = path; - this.observer = new PathObserver(model, path, - this.boundValueChanged, this); + this.path = path || ''; + this.observer = new PathObserver(this.model, this.path, + this.boundValueChanged, + this); this.boundValueChanged(this.value); } @@ -767,9 +768,9 @@ this.node = node; this.property = property; this.model = model; - this.path = path; + this.path = path || '' this.iterator = iterator; - this.iterator.inputs.bind(this.property, model, path || ''); + this.iterator.inputs.bind(this.property, this.model, this.path); } TemplateBinding.prototype = createObject({ @@ -795,8 +796,9 @@ var iterator = TemplateIterator.getOrCreate(this); this.unbind(name); + return this.bindings[name] = - new TemplateBinding(this, name, model, path || '', iterator); + new TemplateBinding(this, name, model, path, iterator); }, createInstance: function(model, delegate, bound) { From c73ae267d5f5dd367dab883b967ff9cb916d7815 Mon Sep 17 00:00:00 2001 From: Eric Bidelman Date: Thu, 1 Aug 2013 17:48:38 -0700 Subject: [PATCH 08/15] Doc has moved --- docs/expression_syntax.md | 98 +-------------------------------------- 1 file changed, 2 insertions(+), 96 deletions(-) diff --git a/docs/expression_syntax.md b/docs/expression_syntax.md index ce3965b..3cb93a8 100644 --- a/docs/expression_syntax.md +++ b/docs/expression_syntax.md @@ -1,99 +1,5 @@ ## Expression Syntax -An ExpressionSyntax is provided as an example of a syntax which is implemented in script using the template element's [Syntax API](https://github.com/Polymer/mdv/blob/master/docs/syntax_api.md). It allows the use of named scopes within template `bind` and `repeat` and simple inline expressions within bindings. +This doc has been moved to https://github.com/Polymer/docs/blob/master/platform/mdv/template.md - * Include the implementation: - -```HTML - -``` - - * Register the syntax for use on the template element (sub-templates will inherit its use). - -```JavaScript -templateElement.bindingDelegate = new ExpressionSyntax(); -``` - - * Use the syntax in your templates - -```HTML - -``` - -## Features - -### Inline expressions - -The ExpressionSyntax allows for inline expressions within bindings which support a strict subset of the JavaScript language. In order to use this feature, it's important to understand its behavior and limitations: - - * The goal for inline expressions is to allow the expression of simple value concepts and relationships. It is generally bad practice to put complex logic into your HTML (view). - * Expressions are never run (e.g. eval) as page script. They cannot access any global state (e.g. window). They are parsed and converted to a simple interpretted form which is provided the present values of paths contained in the expression. - -The specific subset of JavaScript which is supported is: - - * Identifiers & paths, e.g. 'foo', 'foo.bar.baz'. These values are treated as relative to the local model, extracted, observed for changes and cause the expression to be re-evaluated if one or more has changed. - * The logical not operator: ! - * The unary operators: + (Convert to Number), - (Convert to Number and negate). - * The binary operators: + (Addition), - (Subtraction), * (Multiplication), / (Division), % (Modulo). - * Comparators: < (less than), > (greater than), <= (less than or equal), >= (greater then or equal), == (equal), != (not equal), === (identity equally), !== (identity inequality) - * Logical comparators: || (or), && (and) - * Conditional statements: ?. e.g. 'a ? b : c'. - * Grouping (parenthesis): e.g. '(a + b) * (c + d)' - * Literal values: e.g. numbers, strings, null, undefined. Note that escaped strings and non-decimal numbers are not supported. - * Array & Object initializers: e.g. '[foo, 1]', '{ id: 1, foo: bar }' - * Labeled statements: e.g. 'foo: bar.baz; bat: boo > 2' - -When an expression is used within a mustach (`{{` `}}`), it is parsed. The expression should be a single statement, or multiple labeled statements. - -* If the result is a single unlabeled statement, whenever the value of one or more paths in the expression change, the value of the expression re-evaluated and the result inserted as the value of the mustache. e.g. - -```HTML -
Jill has {{ daughter.children.length + son.children.length }} grandchildren
-``` - -* If the result is one or more labeled statements, the value of the mustache will include the set of space-separated label identifiers whose corresponding expressions are truthy. e.g. - -```HTML -
-``` - -### Named scope - -Named scopes are the solution to wanting to reference a model value from an "outer" model "scope". e.g. - -```HTML - -``` - -The scope naming is available (but optional) inside `template` `bind` and `repeat` directives. - - * `bind` syntax: `