Permalink
Browse files
experimental strict refactor of template and bind subsystems
- Loading branch information
Showing
with
767 additions
and 0 deletions.
- +76 −0 src/data/annotations-bind.html
- +275 −0 src/data/bind.html
- +48 −0 src/data/computed.html
- +101 −0 src/data/demo/app-chrome.html
- +52 −0 src/template/demo/app-chrome.html
- +215 −0 src/template/template.html
| @@ -0,0 +1,76 @@ | ||
| <!-- | ||
| @license | ||
| Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | ||
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | ||
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | ||
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | ||
| Code distributed by Google as part of the polymer project is also | ||
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | ||
| --> | ||
| <link rel="import" href="annotations.html"> | ||
|
|
||
| <script> | ||
|
|
||
| /* | ||
| * Parses the annotations list created by `annotations` features to perform | ||
| * declarative desugaring. | ||
| * | ||
| * Depends on `annotations` feature and `bind` feature. | ||
| * | ||
| * Two tasks are supported: | ||
| * | ||
| * - nodes with 'id' are described in a virtual annotation list at | ||
| * registration time. This list is then concretized per instance. | ||
| * | ||
| * - Simple mustache expressions consisting of a single property name | ||
| * in a `textContent` context are bound using `bind` features | ||
| * `bindMethod`. In this mode, the bound method is constructed at | ||
| * registration time, so marshaling is done done via the concretized | ||
| * `_nodes` at every access. | ||
| * | ||
| * TODO(sjmiles): ph3ar general confusion between registration and | ||
| * instance time tasks. Is there a cleaner way to disambiguate? | ||
| */ | ||
| TemplateBind = { | ||
|
|
||
| // construct binding meta-data | ||
|
|
||
| _preprocessBindAnnotations: function(prototype, list) { | ||
| // create a virtual annotation list, must be concretized at instance time | ||
| prototype._nodes = []; | ||
| // process annotations that have been parsed from template | ||
| list.forEach(function(annotation) { | ||
| // where to find the node in the concretized list | ||
| var index = prototype._nodes.push(annotation) - 1; | ||
| // TODO(sjmiles): we need to support multi-bind, right now you only get | ||
| // one (not including kind === `id`) | ||
| annotation.bindings.forEach(function(binding) { | ||
| prototype._bindAnnotationBinding(binding, index); | ||
| }); | ||
| }); | ||
| }, | ||
|
|
||
| // _nodes[index][<binding.name=>]{{binding.value}} | ||
| _bindAnnotationBinding: function(binding, index) { | ||
| // capture the node index | ||
| binding.index = index; | ||
| // discover top-level property (model) from path | ||
| var path = binding.value; | ||
| var i = path.indexOf('.'); | ||
| // [name=]{{model[.subpath]}} | ||
| var model = (i >= 0) ? path.slice(0, i) : path; | ||
| // add 'annotation' binding effect for property 'model' | ||
| this.addPropertyEffect(model, 'annotation', binding); | ||
| } | ||
|
|
||
| // concretize `_nodes` map (annotation based) | ||
|
|
||
| marshalAnnotatedNodes: function(nodes, root) { | ||
| return nodes.map(function(a) { | ||
| return Template.findAnnotatedNode(root, a); | ||
| }; | ||
| } | ||
|
|
||
| }); | ||
|
|
||
| </script> |
| @@ -0,0 +1,275 @@ | ||
| <!-- | ||
| @license | ||
| Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | ||
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | ||
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | ||
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | ||
| Code distributed by Google as part of the polymer project is also | ||
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | ||
| --> | ||
| <script> | ||
|
|
||
| /** | ||
| */ | ||
| Bind = { | ||
|
|
||
| // for instances | ||
|
|
||
| prepareInstance: function(inst) { | ||
| inst._data = Object.create(null); | ||
| }, | ||
|
|
||
| setupBindListeners: function(inst) { | ||
| inst._bindListeners.forEach(this._setupBindListener, this); | ||
| }, | ||
|
|
||
| _setupBindListener: function(info) { | ||
| // <node>.on.<property>-changed: <path]> = e.detail.value | ||
| //console.log('[_setupBindListener]: [%s][%s] listening for [%s][%s-changed]', this.localName, info.path, info.id || info.index, info.property); | ||
| var fn = new Function('e', 'this.' + info.path + ' = e.detail.value;'); | ||
| var node = info.id ? this.$[info.id] : this._nodes[info.index]; | ||
| node.addEventListener(info.property + '-changed', fn.bind(this)); | ||
| }, | ||
|
|
||
| // for prototypes | ||
|
|
||
| // TODO(sjmiles): ad-hoc telemetry | ||
| _telemetry: { | ||
| _setDataCalls: 0 | ||
| }, | ||
|
|
||
| prepareModel: function(model) { | ||
| model._propertyEffects = {}; | ||
| model._bindListeners = {}; | ||
| model._setData = this._setData; | ||
| model._notifyChange = this._notifyChange; | ||
| }, | ||
|
|
||
| _notifyChange: function(property) { | ||
| this.fire(property + '-changed', {value: this[property]}, null, false); | ||
| }, | ||
|
|
||
| _setData: function(property, value) { | ||
| // TODO(sjmiles): ad-hoc telemetry | ||
| //Base._telemetry._setDataCalls++; | ||
| var old = this._data[property]; | ||
| if (old !== value) { | ||
| this._data[property] = value; | ||
| } | ||
| return old; | ||
| }, | ||
|
|
||
| addPropertyEffect: function(model, property, kind, effect) { | ||
| var fx = model._propertyEffects[property]; | ||
| if (!fx) { | ||
| fx = model._propertyEffects[property] = []; | ||
| } | ||
| fx.push({ | ||
| kind: kind, | ||
| effect: effect | ||
| }); | ||
| }, | ||
|
|
||
| addPropertyEffects: function(model, bind) { | ||
| for (var n in bind) { | ||
| var bind = bind[n]; | ||
| if (typeof bind === 'object') { | ||
| // multiplexed definition | ||
| for (var nn in bind) { | ||
| this._addBindEffect(model, n, bind[nn]); | ||
| } | ||
| } else { | ||
| // single definition | ||
| this._addBindEffect(model, n, bind); | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| _addBindEffect: function(model, property, effect) { | ||
| var kind = 'bind'; | ||
| if (typeof this[effect] === 'function') { | ||
| kind = 'method'; | ||
| } | ||
| this.addPropertyEffect(property, kind, effect); | ||
| }, | ||
|
|
||
| createBindings: function(model) { | ||
| var fx$ = model._propertyEffects; | ||
| if (fx$) { | ||
| //console.group(this.name); | ||
| for (var n in fx$) { | ||
| //console.group(n); | ||
| var fx = fx$[n]; | ||
| fx.sort(this._sortPropertyEffects); | ||
| //console.log(fx); | ||
| // | ||
| var compiledEffects = fx.map(function(x) { | ||
| return this._buildEffect(model, n, x); | ||
| }, this); | ||
| // | ||
| this._bindPropertyEffects(model, n, compiledEffects); | ||
| //console.log(fxt.join('\n')); | ||
| //console.groupEnd(); | ||
| } | ||
| //console.groupEnd(); | ||
| } | ||
| }, | ||
|
|
||
| _sortPropertyEffects: function(a, b) { | ||
| if (a.kind === 'compute' || b.kind === 'notify') { | ||
| return -1; | ||
| } | ||
| if (a.kind === 'notify' || b.kind === 'compute') { | ||
| return 1; | ||
| } | ||
| return 0; | ||
| }, | ||
|
|
||
| _buildEffect: function(model, property, fx) { | ||
| return this['_' + fx.kind + 'EffectBuilder'](model, property, fx.effect); | ||
| }, | ||
|
|
||
| // create accessors that implement effects | ||
| _bindPropertyEffects: function(model, property, effects) { | ||
| var defun = { | ||
| get: function() { | ||
| return this._data[property]; | ||
| } | ||
| } | ||
| if (effects.length) { | ||
| // combine effects | ||
| // var group = '\'' + this.name + ':' + property + '\''; | ||
| // effects.unshift('console.group(' + group + ');'); | ||
| // effects.push('console.groupEnd(' + group + ');'); | ||
| effects = effects.join('\n\t\t'); | ||
| // construct effector | ||
| var effector = '_' + property + 'Effector'; | ||
| model[effector] = new Function('old', effects); | ||
| // construct setter body | ||
| var body = | ||
| 'var old = this._setData(\'' + property + '\', value);\n' | ||
| + 'if (value !== old) {\n' | ||
| + ' this.' + effector + '(old);\n' | ||
| + '}'; | ||
| var setter = new Function('value', body); | ||
| // ReadOnly properties have a private setter only | ||
| //if (this.isReadOnlyProperty(property)) { | ||
| // this['_set_' + property] = setter; | ||
| //} | ||
| // other properties have a proper setter | ||
| //else { | ||
| defun.set = setter; | ||
| //} | ||
| } | ||
| Object.defineProperty(model, property, defun); | ||
| //console.log(prop.set ? prop.set.toString() : '(read-only)'); | ||
| }, | ||
|
|
||
| _methodEffectBuilder: function(model, source, effect) { | ||
| // TODO(sjmiles): validation system requires a blessed | ||
| // validator effect which needs to be processed first. | ||
| /* | ||
| if (typeof this[effect] === 'function') { | ||
| return [ | ||
| 'var validated = this.' + effect + '(value, old)', | ||
| 'if (validated !== undefined) {', | ||
| ' // recurse', | ||
| ' this[property] = validated;', | ||
| ' return;', | ||
| '}' | ||
| ].join('\n'); | ||
| } | ||
| */ | ||
| // | ||
| return 'this.' + effect + '(this._data.' + source + ', old);' | ||
| }, | ||
|
|
||
| // basic modus operandi | ||
| // | ||
| // <hostPath> %=% <targetPath> | ||
| // (node = <$.id | nodes[index]>) | ||
| // <model[.path]> %=% node.<property> | ||
| // | ||
| // flow-up: | ||
| // set(model): node.<property> = <model[.path]> | ||
| // | ||
| // flow-down: | ||
| // node.on.<property>-changed: <model[.path]> = e.detail.value | ||
|
|
||
| _bindEffectBuilder: function(model, hostProperty, targetPath) { | ||
| var parts = targetPath.split('.'); | ||
| var id = parts[0], property = parts[1]; | ||
| if (!property) { | ||
| property = 'textContent'; | ||
| // textContent never flows-up | ||
| } else { | ||
| // flow-up | ||
| this._bindListeners.push({ | ||
| id: id, | ||
| property: property, | ||
| path: hostProperty | ||
| }); | ||
| } | ||
| // | ||
| // flow-down | ||
| // | ||
| //console.log('[_bindEffectBuilder]: [%s] %=% [%s].[%s]', hostProperty, id, property); | ||
| return 'this.$.' + id + '.' + property + ' = ' | ||
| + 'this._data.' + hostProperty + ';' | ||
| }, | ||
|
|
||
| _notifyEffectBuilder: function(model, source) { | ||
| return 'this._notifyChange(\'' + source + '\')'; | ||
| }, | ||
|
|
||
| _computeEffectBuilder: function(model, source, effect) { | ||
| return 'this.' + effect.property | ||
| + ' = this.' + effect.method + '(this._data.' + source + ');'; | ||
| }, | ||
|
|
||
| // implement effect directives from template annotations | ||
| // _nodes[info.index][info.name] = {{info.value}} | ||
| _annotationEffectBuilder: function(model, hostProperty, info) { | ||
| var property = info.name || 'textContent'; | ||
| if (property !== 'textContent') { | ||
| // <node>.on.<property>-changed: <path> = e.detail.value | ||
| this._addAnnotatedListener(model, info.index, property, info.value); | ||
| } | ||
| // | ||
| // flow-down | ||
| // | ||
| // construct the effect to occur when [property] changes: | ||
| // set nodes[index][name] to this[value] | ||
| // | ||
| //console.log('[_annotationEffectBuilder]: [%s] %=% [%s].[%s]', info.value, info.index, property); | ||
| return 'this._nodes[' + info.index + '].' + property | ||
| + ' = this._data.' + info.value + ';'; | ||
| }, | ||
|
|
||
| // end of builders | ||
|
|
||
| _addAnnotatedListener: function(model, index, property, path) { | ||
| // <node>.on.<property>-changed: <path> = e.detail.value | ||
| model._bindListeners.push({ | ||
| index: index, | ||
| property: property, | ||
| path: path | ||
| }); | ||
| }, | ||
|
|
||
| _bindAnnotationProperty: function(name, path, index) { | ||
| return 'this._nodes[' + index + '].' + name | ||
| + ' = this._data.' + path + ';'; | ||
| }, | ||
|
|
||
| _addBindListener: function(property, path, id) { | ||
| var bl = this._requireBindListeners(property); | ||
| bl.targets.push({ | ||
| id: id, | ||
| path: path | ||
| }); | ||
| } | ||
|
|
||
| }; | ||
|
|
||
| </script> |
Oops, something went wrong.