diff --git a/PRIMER.md b/PRIMER.md index 908abc18..538fcce8 100644 --- a/PRIMER.md +++ b/PRIMER.md @@ -1103,7 +1103,7 @@ Values will be serialized according to type: Arrays/Objects will be `JSON.string ## 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 @@ -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 ``` +Note: Only direct properties of the element (as opposed to sub-properties of an object) can be used as dependencies at this time. + ## 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 `.isManager == true` OR `.mode == 2`, you want to set `.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 - - - - - -``` - -Due to the synchronous nature of bindings in 0.8, code such as the following will result in `.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: diff --git a/src/lib/bind/bind-effects.html b/src/lib/bind/bind-effects.html index c037ea22..eab77273 100644 --- a/src/lib/bind/bind-effects.html +++ b/src/lib/bind/bind-effects.html @@ -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 diff --git a/test/unit/bind.html b/test/unit/bind.html index 491b3088..56eed767 100644 --- a/test/unit/bind.html +++ b/test/unit/bind.html @@ -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(); + }); }); });