Skip to content

Commit

Permalink
Add debounced multiple computed property support.
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpschaaf committed Feb 24, 2015
1 parent d61f407 commit 9d5ee38
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 81 deletions.
59 changes: 11 additions & 48 deletions PRIMER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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;
}
...
Expand All @@ -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

Expand Down Expand Up @@ -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

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.

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):
## Compound observation

```js
myParent.isManager = false;
myParent.mode = 5;
```
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.

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:

Expand Down
14 changes: 8 additions & 6 deletions src/lib/bind/bind-effects.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
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
Expand Down Expand Up @@ -99,8 +102,7 @@
},

compute: function(model, source, effect) {
return 'this.' + effect.property
+ ' = this.' + effect.method + '(this._data.' + source + ');';
return effect;
},

reflect: function(model, source) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bind/bind.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
// 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);
Expand Down
16 changes: 13 additions & 3 deletions test/unit/bind-elements.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
type: Number,
notify: true
},
computedFromMultipleValues: {
type: Number,
notify: true
},
camelNotifyingValue: {
type: Number,
notify: true
Expand All @@ -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) {
Expand All @@ -42,7 +48,11 @@
readonlyvalueChanged: function() {},
computeNotifyingValue: function(val) {
return val + 2;
}
},
computeFromMultipleValues: function(sum1, sum2, divide) {
return (sum1 + sum2) / divide;
},
computedFromMultipleValuesChanged: function() {}
});
</script>

Expand Down
99 changes: 76 additions & 23 deletions test/unit/bind.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@
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() {
Expand All @@ -79,23 +82,49 @@
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() {
Expand Down Expand Up @@ -172,19 +201,25 @@
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() {
Expand All @@ -206,18 +241,24 @@
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() {
Expand Down Expand Up @@ -274,19 +315,25 @@
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() {
Expand All @@ -305,19 +352,25 @@
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();
});
});

});
Expand Down

0 comments on commit 9d5ee38

Please sign in to comment.