Skip to content
Permalink
Browse files

experimental strict refactor of template and bind subsystems

  • Loading branch information
sjmiles committed Dec 9, 2014
1 parent 993a2cc commit 2198d4a91db788c044763ec28f95dfcb6c57305e
@@ -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>

0 comments on commit 2198d4a

Please sign in to comment.
You can’t perform that action at this time.