Add debounced multiple computed property support.

This commit is contained in:
Kevin Schaaf
2015-02-23 20:04:16 -08:00
parent d61f407205
commit 9d5ee38f02
5 changed files with 109 additions and 81 deletions

View File

@@ -1103,7 +1103,7 @@ Values will be serialized according to type: Arrays/Objects will be `JSON.string
<a name="computed-properties"></a>
## Computed properties
Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined by providing an object-valued `computed` property on the prototype that maps property names to computing functions. The name of the function to compute the value is provided as a string with dependent properties as arguments in parenthesis. Only one dependency is supported at this time.
Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined by providing an object-valued `computed` property on the prototype that maps property names to computing functions. The name of the function to compute the value is provided as a string with dependent properties as arguments in parenthesis. The function will be called once (asynchronously) for any change to the dependent properties.
```html
<dom-module id="x-custom">
@@ -1118,13 +1118,13 @@ Polymer supports virtual properties whose values are calculated from other prope
is: 'x-custom',
computed: {
// when `user` changes `computeFullName` is called and the
// value it returns is stored as `fullName`
fullName: 'computeFullName(user)',
// when `first` or `last` changes `computeFullName` is called once
// (asynchronously) and the value it returns is stored as `fullName`
fullName: 'computeFullName(first, last)',
},
computeFullName: function(user) {
return user.firstName + ' ' + user.lastName;
computeFullName: function(first, last) {
return first + ' ' + last;
}
...
@@ -1133,6 +1133,8 @@ Polymer supports virtual properties whose values are calculated from other prope
</script>
```
Note: Only direct properties of the element (as opposed to sub-properties of an object) can be used as dependencies at this time.
<a name="read-only"></a>
## Read-only properties
@@ -1304,50 +1306,11 @@ Current limitations that are on the backlog for evaluation/improvement are liste
* Support for compound property binding
* See below
## Compound property effects
## Compound observation
Polymer 0.8 currently has no built-in support for compound observation or compound binding expressions. This problem space is on the backlog to be tackled in the near future. This section will discuss lower-level tools that are available in 0.8 that can be used instead.
Polymer 0.8 does not currently support observer functions called once for changes to a set of dependent properties, outside of computed properties. If the work of computing the property is expensive, or if the side-effects of the binding are expensive, then you may want to ensure side-effects only occur once for any number of changes to them during a turn by manually introducing asynchronicity. The `computed` property feature uses `debounce` under the hood to achieve the same effect.
Assume an element has a boolean property that should be set when either of two conditions are true: e.g. when `<my-parent>.isManager == true` OR `<my-parent>.mode == 2`, you want to set `<my-child>.disabled = true`.
The most naive way to achieve this in 0.8 is with separate change handlers for the dependent properties that set a `shouldDisable` property bound to the `my-child`.
Example:
```html
<dom-module id="x-parent">
<template>
<x-child disabled="{{shouldDisable}}"></my-child>
</template>
</dom-module>
<script>
Polymer({
is: 'x-parent',
bind: {
isManager: 'computeShouldDisable',
mode: 'computeShouldDisable',
},
// Warning: Called once for every change to dependent properties!
computeShouldDisable: function() {
this.shouldDisable = this.isManager || (this.mode == 2);
}
});
</script>
```
Due to the synchronous nature of bindings in 0.8, code such as the following will result in `<my-child>.disabled` being set twice (and any side-effects of that property changing to potentially occur twice):
```js
myParent.isManager = false;
myParent.mode = 5;
```
If the work of computing the property is expensive, or if the side-effects of the binding are expensive, then you may want to ensure side-effects only occur once for any number of changes to them during a turn by manually introducing asynchronicity. The `debounce` API on the Polymer Base prototype can be used to achieve this. The `debounce` API takes a signal name (String), callback, and optional wait time, and only calls the callback once for any number `debounce` calls with the same `signalName` started within the wait period.
The `debounce` API on the Polymer Base prototype can be used to achieve this. The `debounce` API takes a signal name (String), callback, and optional wait time, and only calls the callback once for any number `debounce` calls with the same `signalName` started within the wait period.
Example:

View File

@@ -16,10 +16,13 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
var method = expression.slice(0, index);
var args = expression.slice(index + 1, -1).replace(/ /g, '').split(',');
//console.log('%c on [%s] compute [%s] via [%s]', 'color: green', args[0], name, method);
this.addPropertyEffect(model, args[0], 'compute', {
property: name,
method: method
});
var methodArgs = 'this._data.' + args.join(', this._data.');
var methodString = 'this.debounce(\'_' + method + '\', function() {\n' +
'\t\tthis.' + name + ' = this.' + method + '(' + methodArgs + ');\n' +
'\t});';
for (var i=0; i<args.length; i++) {
this.addPropertyEffect(model, args[i], 'compute', methodString);
}
};
// TODO(sjmiles): case shenanigans
@@ -99,8 +102,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
},
compute: function(model, source, effect) {
return 'this.' + effect.property
+ ' = this.' + effect.method + '(this._data.' + source + ');';
return effect;
},
reflect: function(model, source) {

View File

@@ -117,7 +117,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
// var group = '\'' + this.is + ':' + property + '\'';
// effects.unshift('console.group(' + group + ');');
// effects.push('console.groupEnd(' + group + ');');
effects = effects.join('\n\t\t');
effects = '\t' + effects.join('\n\t');
// construct effector
var effector = '_' + property + 'Effector';
model[effector] = new Function('old', effects);

View File

@@ -13,6 +13,10 @@
type: Number,
notify: true
},
computedFromMultipleValues: {
type: Number,
notify: true
},
camelNotifyingValue: {
type: Number,
notify: true
@@ -25,13 +29,15 @@
},
computed: {
computedvalue: 'computeValue(value)',
computednotifyingvalue: 'computeNotifyingValue(notifyingvalue)'
computednotifyingvalue: 'computeNotifyingValue(notifyingvalue)',
computedFromMultipleValues: 'computeFromMultipleValues(sum1, sum2, divide)'
},
bind: {
value: 'valueChanged',
computedvalue: 'computedvalueChanged',
notifyingvalue: 'notifyingvalueChanged',
readonlyvalue: 'readonlyvalueChanged'
readonlyvalue: 'readonlyvalueChanged',
computedFromMultipleValues: 'computedFromMultipleValuesChanged'
},
valueChanged: function() {},
computeValue: function(val) {
@@ -42,7 +48,11 @@
readonlyvalueChanged: function() {},
computeNotifyingValue: function(val) {
return val + 2;
}
},
computeFromMultipleValues: function(sum1, sum2, divide) {
return (sum1 + sum2) / divide;
},
computedFromMultipleValuesChanged: function() {}
});
</script>

View File

@@ -58,10 +58,13 @@ suite('single-element binding effects', function() {
assert.equal(called, true, 'Change handler not called');
});
test('computed value updates', function() {
test('computed value updates', function(done) {
el.value = 44;
assert.equal(el.computedvalue, 45, 'Computed value not correct');
assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child');
setTimeout(function() {
assert.equal(el.computedvalue, 45, 'Computed value not correct');
assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child');
done();
});
});
test('notification sent', function() {
@@ -79,23 +82,49 @@ suite('single-element binding effects', function() {
assert.equal(notified, 2, 'Notification events not sent');
});
test('computed change handler called', function() {
test('computed change handler called', function(done) {
var called = false;
el.computedvalueChanged = function() {
called = true;
};
el.value = 46;
assert.equal(called, true, 'Change handler not called');
setTimeout(function() {
assert.equal(called, true, 'Change handler not called');
done();
});
});
test('computed notification sent', function() {
test('computed notification sent', function(done) {
var notified = false;
el.addEventListener('computednotifyingvalue-changed', function(e) {
assert.equal(e.detail.value, 49);
notified = true;
});
el.notifyingvalue = 47;
assert.equal(notified, true, 'Notification event not sent');
setTimeout(function() {
assert.equal(notified, true, 'Notification event not sent');
done();
});
});
test('computed property with multiple dependencies', function(done) {
var called = false;
el.computedFromMultipleValuesChanged = function() {
called = true;
};
var notified = false;
el.addEventListener('computed-from-multiple-values-changed', function(e) {
notified = true;
});
el.sum1 = 10;
el.sum2 = 20;
el.divide = 2;
setTimeout(function() {
assert.equal(el.computedFromMultipleValues, 15, 'Computed value wrong');
assert.equal(notified, true, 'Notification event not sent');
assert.equal(called, true, 'Change handler not called');
done();
});
});
test('no read-only change handler called with assignment', function() {
@@ -172,19 +201,25 @@ suite('2-way binding effects between elements', function() {
assert.equal(called, false, 'changed handler for property bound to non-notifying property called and should not have been');
});
test('binding to non-notifying computed property', function() {
test('binding to non-notifying computed property', function(done) {
el.boundcomputedvalue = 42;
el.$.basic1.value = 43;
assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been');
setTimeout(function() {
assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been');
done();
});
});
test('changed handler for property bound to non-notifying computed property', function() {
test('changed handler for property bound to non-notifying computed property', function(done) {
var called = false;
el.boundcomputedvalueChanged = function() {
called = true;
};
el.$.basic1.value = 44;
assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been');
setTimeout(function() {
assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been');
done();
});
});
test('binding to notifying property', function() {
@@ -206,18 +241,24 @@ suite('2-way binding effects between elements', function() {
assert.equal(called, true, 'changed handler for property bound to notifying property not called');
});
test('binding to notifying computed property', function() {
test('binding to notifying computed property', function(done) {
el.$.basic1.notifyingvalue = 43;
assert.equal(el.boundcomputednotifyingvalue, 45, 'binding to notifying computed property not updated');
setTimeout(function() {
assert.equal(el.boundcomputednotifyingvalue, 45, 'binding to notifying computed property not updated');
done();
});
});
test('changed handler for property bound to notifying computed property', function() {
test('changed handler for property bound to notifying computed property', function(done) {
var called = false;
el.boundcomputednotifyingvalueChanged = function() {
called = true;
};
el.$.basic1.notifyingvalue = 45;
assert.equal(called, true, 'changed handler for property bound to non-notifying computed property not called');
setTimeout(function() {
assert.equal(called, true, 'changed handler for property bound to non-notifying computed property not called');
done();
});
});
test('no change for binding into read-only property', function() {
@@ -274,19 +315,25 @@ suite('1-way binding effects between elements', function() {
assert.equal(called, false, 'changed handler for property one-way-bound to non-notifying property called and should not have been');
});
test('one-way binding to non-notifying computed property', function() {
test('one-way binding to non-notifying computed property', function(done) {
el.boundcomputedvalue = 42;
el.$.basic2.value = 43;
assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been');
setTimeout(function() {
assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been');
done();
});
});
test('changed handler for property one-way-bound to non-notifying computed property', function() {
test('changed handler for property one-way-bound to non-notifying computed property', function(done) {
var called = false;
el.boundcomputedvalueChanged = function() {
called = true;
};
el.$.basic2.value = 44;
assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been');
setTimeout(function() {
assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been');
done();
});
});
test('one-way binding to notifying property', function() {
@@ -305,19 +352,25 @@ suite('1-way binding effects between elements', function() {
assert.equal(called, false, 'changed handler for property bound to notifying property called and should not have been');
});
test('one-way binding to notifying computed property', function() {
test('one-way binding to notifying computed property', function(done) {
el.boundcomputednotifyingvalue = 42;
el.$.basic2.notifyingvalue = 43;
assert.equal(el.boundcomputednotifyingvalue, 42, 'binding to notifying computed property updated and should not have been');
setTimeout(function() {
assert.equal(el.boundcomputednotifyingvalue, 42, 'binding to notifying computed property updated and should not have been');
done();
});
});
test('changed handler for property one-way-bound to notifying computed property', function() {
test('changed handler for property one-way-bound to notifying computed property', function(done) {
var called = false;
el.boundcomputednotifyingvalueChanged = function() {
called = true;
};
el.$.basic2.notifyingvalue = 45;
assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been');
setTimeout(function() {
assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been');
done();
});
});
});