diff --git a/PRIMER.md b/PRIMER.md index 6cf9e57fab..5569941f4b 100644 --- a/PRIMER.md +++ b/PRIMER.md @@ -740,6 +740,8 @@ Example: ## Property change callbacks (observers) +### Single property observation + Custom element properties may be observed for changes by specifying `observer` property in the `properties` for the property that gives the name of a funciton to call. When the property changes, the change handler will be called with the new and old values as arguments. Example: @@ -774,9 +776,15 @@ Polymer({ }); ``` -Property change observation is achieved in Polymer by installing setters on the custom element prototype for properties with registered interest (as opposed to observation via Object.observe or dirty checking, for example). +Note that property change observation is achieved in Polymer by installing setters on the custom element prototype for properties with registered interest (as opposed to observation via Object.observe or dirty checking, for example). + +### Multipe property observation + +Observing changes to multiple properties is supported via the `observers` array on the prototype, using a string containing a method signature that includes any dependent arguments. Once all properties are defined (`!== undefined`), the observer method will be called once for each change to a dependent property. The current values of the dependent properties will be passed as arguments to the observer method in the order defined in the `observers` method signature. + +*Note, computing functions will only be called once all dependent properties are defined (`!=undefined`). If one or more of the properties are optional, they would need default `value`'s defined in `properties` to ensure the observer is called.* -Observing changes to multiple properties is supported via the `observers` object, by specifying a string-separated list of dependent properties that should result in a change function being called. These observers differ from single-property observers in that the change handler is called asynchronously. +*Note that any observers defined in the `observers` array will not receive `old` values as arguments, only new values. Only single-property observers defined in the `properties` object received both `old` and `new` values.* Example: @@ -791,9 +799,9 @@ Polymer({ size: String }, - observers: { - 'preload src size': 'updateImage' - }, + observers: [ + 'updateImage(preload, src, size)' + ], updateImage: function(preload, src, size) { // ... do work using dependent values @@ -802,7 +810,9 @@ Polymer({ }); ``` -Additionally, observing changes to object sub-properties is also supported via the same `observers` object, by specifying a full (e.g. `user.manager.name`) or partial path (`user.*`) and function name to call. In this case, the third argument will indicate the path that changed. Note that currently the second argument (old value) will not be valid. +### Path observation + +Observing changes to object sub-properties is also supported via the same `observers` array, by specifying a path (e.g. `user.manager.name`). Example: @@ -815,25 +825,51 @@ Polymer({ user: Object }, - observers: { - 'user.manager.*': 'userManagerChanged' + observers: [ + 'userManagerChanged(user.manager)' + ], + + userManagerChanged: function(user) { + console.log('new manager name is ' + user.name); + } + +}); +``` + +*Note that observing changes to paths (object sub-properties) is dependent on one of two requirements: either the value at the path in question changed via a Polymer [property binding](#property-binding) to another element, or the value was changed using the [`setPathValue`](#set-path) API, which provides the required notification to elements with registered interest.* + +### Deep path observation + +Additionally, wildcard matching of path changes is also supported via the `observers` array, which allows notification when any (deep) sub-property of an object changes. Note that the argument passed for a path with a wildcard is a change record object containing the `path` that changed, the new `value` of the path that changed, and the `base` value of the wildcard expression. + +Example: + +```js +Polymer({ + + is: 'x-custom', + + properties: { + user: Object }, - userManagerChanged: function(newValue, oldValue, path) { - if (path) { - // sub-property of user.manager changed - console.log('manager ' + path.split('.').pop() + ' changed to ' + newValue); - } else { + observers: [ + 'userManagerChanged(user.manager.*)' + ], + + userManagerChanged: function(changeRecord) { + if (changeRecord.path == 'user.manager') { // user.manager object itself changed console.log('new manager name is ' + newValue.name); + } else { + // sub-property of user.manager changed + console.log(changeRecord.path + ' changed to ' + changeRecord.value); } } }); ``` -Note that observing changes to paths (object sub-properties) is dependent on one of two requirements: either the value at the path in question changed via a Polymer [property binding](#property-binding) to another element, or the value was changed using the [`setPathValue`](#set-path) API, which provides the required notification to elements with registered interest. - ## Annotated property binding @@ -1245,7 +1281,9 @@ Values will be serialized according to type; by default Arrays/Objects will be ` ## Computed properties -Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined in the `properties` object by providing a `computed` key mapping to a computing function. 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. +Polymer supports virtual properties whose values are calculated from other properties. Computed properties can be defined in the `properties` object by providing a `computed` key mapping to a computing function. The name of the function to compute the value is provided as a string with dependent properties as arguments in parenthesis. Once all properties are defined (`!== undefined`), the computing function will be called to update the computed property once for each change to a dependent property. + +*Note, computing functions will only be called once all dependent properties are defined (`!=undefined`). If one or more of the properties are optional, they would need default `value`'s defined in `properties` to ensure the property is computed.* ```html @@ -1289,7 +1327,11 @@ Note: Only direct properties of the element (as opposed to sub-properties of an ## Annotated computed properties -Anonymous computed properties may also be placed directly in template binding annotations. This is useful when the property need not be a part of the element's API or otherwise used by logic in the element, and is only used for downward data propagation. Note: this is the only form of functions allowed in template bindings. +Anonymous computed properties may also be placed directly in template binding annotations. This is useful when the property need not be a part of the element's API or otherwise used by logic in the element, and is only used for downward data propagation. + +*Note: this is the only form of functions allowed in template bindings, and they must specify one or more dependent properties as arguments, otherwise the function will not be called.* + +*Note, computing functions will only be called once all dependent properties are defined (`!=undefined`). If one or more of the properties are optional, they would need default `value`'s defined in `properties` to ensure the property is computed.* Example: @@ -1459,6 +1501,8 @@ Then the `observe` property should be configured as follows: filter="isEngineer" observe="type manager.type"> ``` +Note, to reach the outer parent scope, bindings in an `x-repeat` template may be prefixed with `parent.`. + ## Array selector (x-array-selector) EXPERIMENTAL - API MAY CHANGE @@ -1507,6 +1551,53 @@ Keeping structured data in sync requires that Polymer understand the path associ ``` + +## Conditional template +EXPERIMENTAL - API MAY CHANGE + +Elements can be conditionally stamped based on a boolean property by wrapping them in a custom `HTMLTemplateElement` type extension called `x-if`. The `x-if` template stamps itself into the DOM only when its `if` property becomes truthy. + +If the `if` property becomes falsy again, by default all stamped elements will be hidden (but will remain in DOM) for faster performance should the `if` property become truthy again. This behavior may be defeated by setting the `restamp` property, which results in slower `if` switching behavior as the elements are destroyed and re-stamped each time. + +Note, to reach the outer parent scope, all bindings in an `x-if` template must be prefixed with `parent.`, as shown below. + +Example: + +**Note, this is a simple example for illustrative purposes only. Read below for guidance on recommended usage of conditional templates.** + +```html + + + + + + + +``` + +Note, since it is generally much faster to hide/show elements rather than create/destroy them, conditional templates are only useful to save initial creation cost when the elements being stamped are relatively heavyweight and the conditional may rarely (or never) be true in given useages. Otherwise, liberal use of conditional templates can actually *add* significant runtime performance overhead. + +Consider an app with 4 screens, plus an optional admin screen. If most users will use all 4 screens during normal use of the app, it is generally better to incur the cost of stamping those elements once at startup (where some app initialization time is expected) and simply hide/show the screens as the user navigates through the app, rather than re-create and destroy all the elements of each screen as the user navigates. Using a conditional template here may be a poor choice, since although it may save time at startup by stamping only the first screen, that saved time gets shifted to runtime latency for each user interaction, since the time to show the second screen will be *slower* as it must create the second screen from scratch rather than simply showing that screen. Hiding/showing elements is as simple as attribute-binding to the `hidden` attribute (e.g. `
`), and does not require conditional templating at all. + +However, using a conditional template may be appropriate in the case of an admin screen that should only be shown to admin users of an app. Since most users would not be admins, there may be performance benefits to not burdening most of the users with the cost of stamping the elements for the admin page, especially if it is relatively heavyweight. + ## Auto-binding template EXPERIMENTAL - API MAY CHANGE diff --git a/src/lib/annotations/annotations.html b/src/lib/annotations/annotations.html index d02883a070..6e99014baa 100644 --- a/src/lib/annotations/annotations.html +++ b/src/lib/annotations/annotations.html @@ -153,7 +153,7 @@ }, // 1. Parse annotations from the template and memoize them on - // content._annotes (recurses into nested templates) + // content._notes (recurses into nested templates) // 2. Parse template bindings for parent.* properties and memoize them on // content._parentProps // 3. Create bindings in current scope's annotation list to template for @@ -167,7 +167,7 @@ // TODO(sjmiles): simply altering the .content reference didn't // work (there was some confusion, might need verification) var content = document.createDocumentFragment(); - content._annotes = this.parseAnnotations(node); + content._notes = this.parseAnnotations(node); content.appendChild(node.content); // Special-case treatment of 'parent.*' props for nested templates // Automatically bind `prop` on host to `_parent_prop` on template @@ -206,7 +206,7 @@ // template) are stored in content._parentProps. _discoverTemplateParentProps: function(content) { var chain = content._parentPropChain = []; - content._annotes.forEach(function(n) { + content._notes.forEach(function(n) { // Find all bindings to parent.* and spread them into _parentPropChain n.bindings.forEach(function(b) { var m; diff --git a/src/lib/bind/accessors.html b/src/lib/bind/accessors.html index 52f19a4d82..02c8d4e135 100644 --- a/src/lib/bind/accessors.html +++ b/src/lib/bind/accessors.html @@ -123,7 +123,8 @@ 'reflect': 3, 'notify': 4, 'observer': 5, - 'function': 6 + 'complexObserver': 6, + 'function': 7 }; return function(a, b) { return EFFECT_ORDER[a.kind] - EFFECT_ORDER[b.kind]; diff --git a/src/lib/bind/effects.html b/src/lib/bind/effects.html index 755796035e..d33e8f955b 100644 --- a/src/lib/bind/effects.html +++ b/src/lib/bind/effects.html @@ -12,21 +12,21 @@ Polymer.Base.extend(Polymer.Bind, { - _shouldAddListener: function(info) { - return info.name && - info.mode === '{' && - !info.negate && - info.kind != 'attribute' + _shouldAddListener: function(effect) { + return effect.name && + effect.mode === '{' && + !effect.negate && + effect.kind != 'attribute' ; }, - annotationEffect: function(source, value, info) { - if (source != info.value) { - value = this.getPathValue(info.value); - this._data[info.value] = value; + annotationEffect: function(source, value, effect) { + if (source != effect.value) { + value = this.getPathValue(effect.value); + this._data[effect.value] = value; } - var calc = info.negate ? !value : value; - return this._applyEffectValue(calc, info); + var calc = effect.negate ? !value : value; + return this._applyEffectValue(calc, effect); }, reflectEffect: function(source) { @@ -37,50 +37,67 @@ this._notifyChange(source); }, - // Raw effect for extension; info.function is an actual function - functionEffect: function(source, value, info, old) { - info.function.call(this, source, value, info, old); + // Raw effect for extension; effect.function is an actual function + functionEffect: function(source, value, effect, old) { + effect.function.call(this, source, value, effect, old); }, - observerEffect: function(source, value, info, old) { - //console.log(value, info); - if (info.property) { - this[info.method](value, old); - } else { - var args = Polymer.Bind._marshalArgs(this._data, info.properties); - if (args) { - this[info.method].apply(this, args); - } + observerEffect: function(source, value, effect, old) { + this[effect.method](value, old); + }, + + complexObserverEffect: function(source, value, effect) { + var args = Polymer.Bind._marshalArgs(this._data, effect, source, value); + if (args) { + this[effect.method].apply(this, args); } }, - computeEffect: function(source, value, info) { - var args = Polymer.Bind._marshalArgs(this._data, info.args); + computeEffect: function(source, value, effect) { + var args = Polymer.Bind._marshalArgs(this._data, effect, source, value); if (args) { - this[info.property] = this[info.methodName].apply(this, args); + this[effect.property] = this[effect.method].apply(this, args); } }, - annotatedComputationEffect: function(source, value, info) { - var args = Polymer.Bind._marshalArgs(this._data, info.args); + annotatedComputationEffect: function(source, value, effect) { + var args = Polymer.Bind._marshalArgs(this._data, effect, source, value); if (args) { var computedHost = this._rootDataHost || this; var computedvalue = - computedHost[info.methodName].apply(computedHost, args); - this._applyEffectValue(computedvalue, info); + computedHost[effect.method].apply(computedHost, args); + this._applyEffectValue(computedvalue, effect); } }, - _marshalArgs: function(model, properties) { - var a=[]; - for (var i=0, l=properties.length, v; i /** - * Support for the declarative property sugaring via mustache `{{ }}` - * annotations in templates, and via the `properties` objects on - * prototypes. + * Support for property side effects. + * + * Key for effect objects: * - * Example: - * - * - * - * The `properties` object syntax is as follows: - * - * Polymer({ - * - * properties: { - * myProp: { - * observer: 'myPropChanged', - * computed: 'computemyProp(input1, input2)' - * } - * } - * - * ... - * - * }); - * - * The `bind` feature also provides an API for registering effects against - * properties. - * - * Property effects can be created imperatively, by template-annotations - * (e.g. mustache notation), or by declaration in the `properties` object. - * - * The effect data is consumed by the `bind` subsystem (`/src/bind/*`), - * which compiles the effects into efficient JavaScript that is triggered, - * e.g., when a property is set to a new value. - * - * @class data feature: bind + * property | ann | anCmp | cmp | obs | cplxOb | description + * ---------|-----|-------|-----|-----|--------|---------------------------------------- + * method | | X | X | X | X | function name to call on instance + * args | | X | X | | X | list of all arg descriptors for fn + * arg | | X | X | | X | arg descriptor for effect + * property | | | X | X | | property for effect to set or get + * name | X | | | | | annotation value (text inside {{...}}) + * kind | X | X | | | | binding type (property or attribute) + * index | X | X | | | | node index to set + * */ Polymer.Base.addFeature({ @@ -66,8 +44,8 @@ _prepEffects: function() { Polymer.Bind.prepareModel(this); this._addPropertyEffects(this.properties); - this._addObserverEffects(this.observers); - this._addAnnotationEffects(this._annotes); + this._addComplexObserverEffects(this.observers); + this._addAnnotationEffects(this._notes); Polymer.Bind.createBindings(this); }, @@ -91,96 +69,110 @@ } }, - _addComputedEffect: function(name, expression) { - var index = expression.indexOf('('); - 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); - var effect = { - property: name, - args: args, - methodName: method - }; - for (var i=0; i 0; + if (a.structured) { + a.wildcard = (arg.slice(-2) == '.*'); + if (a.wildcard) { + a.name = arg.slice(0, -2); + } } + return a; + }, + + _addComputedEffect: function(name, expression) { + var sig = this._parseMethod(expression); + sig.args.forEach(function(arg) { + this._addPropertyEffect(arg.name, 'compute', { + method: sig.method, + args: sig.args, + arg: arg, + property: name + }); + }, this); }, _addObserverEffect: function(property, observer) { - var effect = { - method: observer - }; - var props = property.split(' '); - if (props.length == 1) { - // Single property synchronous observer (supports paths) - var model = property.split('.').shift(); - if (model != property) { - // TODO(kschaaf): path observers won't get the right `new` argument - this.addPathObserver(property, observer); - } - effect.property = model; - this._addPropertyEffect(model, 'observer', effect); - } else { - // Multiple-property observer - effect.properties = props; - for (var i=0, l=props.length; i.on.-changed: = e.detail.value Polymer.Bind._addAnnotatedListener(this, index, - effect.name, effect.value, effect.event); + note.name, note.value, note.event); } - var computed = effect.value.match(/(\w*)\((.*)\)/); - if (computed) { - var method = computed[1]; - var args = computed[2].split(/[^\w]+/); - this._addAnnotatedComputationEffect(method, args, effect, index); + var sig = this._parseMethod(note.value); + if (sig) { + this._addAnnotatedComputationEffect(sig, note, index); } else { // capture the node index - effect.index = index; + note.index = index; // discover top-level property (model) from path - var model = effect.value.split('.').shift(); + var model = note.value.split('.').shift(); // add 'annotation' binding effect for property 'model' - this._addPropertyEffect(model, 'annotation', effect); + this._addPropertyEffect(model, 'annotation', note); } }, - _addAnnotatedComputationEffect: function(method, args, info, index) { - var effect = { - kind: info.kind, - property: info.name, - index: index, - args: args, - methodName: method - }; - for (var i=0, l=args.length; i[.textContent]) + exclaim: { + observer: 'exclaim' + }, + state: { + observer: 'state' + } }, - observers: { - // property: target (set property is pushed to $.[.textContent]) - exclaim: 'exclaim', - state: 'state' + listeners: { + click: 'clickAction' }, created: function() { diff --git a/test/unit/bind-elements.html b/test/unit/bind-elements.html index afc51e4cad..bb1102d6cf 100644 --- a/test/unit/bind-elements.html +++ b/test/unit/bind-elements.html @@ -36,7 +36,7 @@ notifyingvalue: { type: Number, notify: true, - computed: 'notifyingvalueChanged' + observer: 'notifyingvalueChanged' }, computednotifyingvalue: { type: Number, @@ -74,31 +74,110 @@ value: function() { return {}; } } }, - observers: { - 'dep1 dep2 dep3': 'multipleDepChangeHandler', - 'customEventObject.value': 'customEventObjectValueChanged' + observers: [ + 'multipleDepChangeHandler(dep1 dep2 dep3)', + 'customEventObjectValueChanged(customEventObject.value)' + ], + created: function() { + this.observerCounts = { + valueChanged: 0, + computedvalueChanged: 0, + computedvaluetwoChanged: 0, + notifyingvalueChanged: 0, + readonlyvalueChanged: 0, + computedFromMultipleValuesChanged: 0, + multipleDepChangeHandler: 0, + customEventValueChanged: 0, + customEventObjectValueChanged: 0 + }; + }, + clearObserverCounts: function() { + for (var i in this.observerCounts) { + this.observerCounts[i] = 0; + } + }, + valueChanged: function(val, old) { + this.observerCounts.valueChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.value, 'observer value argument wrong'); + assert.equal(old, this._value, 'observer old argument wrong'); + this._value = val; }, - valueChanged: function() {}, computeValue: function(val) { return val + 1; }, - computedvalueChanged: function() {}, - computedvaluetwoChanged: function() {}, - notifyingvalueChanged: function() {}, - readonlyvalueChanged: function() {}, + computedvalueChanged: function(val, old) { + this.observerCounts.computedvalueChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.computedvalue, 'observer value argument wrong'); + assert.equal(old, this._computedvalue, 'observer old argument wrong'); + this._computedvalue = val; + }, + computedvaluetwoChanged: function(val, old) { + this.observerCounts.computedvaluetwoChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.computedvaluetwo, 'observer value argument wrong'); + assert.equal(old, this._computedvaluetwo, 'observer old argument wrong'); + this._computedvaluetwo = val; + }, + notifyingvalueChanged: function(val, old) { + this.observerCounts.notifyingvalueChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.notifyingvalue, 'observer value argument wrong'); + assert.equal(old, this._notifyingvalue, 'observer old argument wrong'); + this._notifyingvalue = val; + }, + readonlyvalueChanged: function(val, old) { + this.observerCounts.readonlyvalueChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.readonlyvalue, 'observer value argument wrong'); + assert.equal(old, this._readonlyvalue, 'observer old argument wrong'); + this._readonlyvalue = val; + }, computeNotifyingValue: function(val) { return val + 2; }, computeFromMultipleValues: function(sum1, sum2, divide) { + assert.equal(arguments.length, 3, 'observer argument length wrong'); + assert.equal(sum1, this.sum1, 'observer value argument wrong'); + assert.equal(sum2, this.sum2, 'observer value argument wrong'); + assert.equal(divide, this.divide, 'observer value argument wrong'); return (sum1 + sum2) / divide; }, - computedFromMultipleValuesChanged: function() {}, - multipleDepChangeHandler: function() {}, + computedFromMultipleValuesChanged: function(val, old) { + this.observerCounts.computedFromMultipleValuesChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.computedFromMultipleValues, 'observer value argument wrong'); + assert.equal(old, this._computedFromMultipleValues, 'observer old argument wrong'); + this._computedFromMultipleValues = val; + }, + multipleDepChangeHandler: function(dep1, dep2, dep3) { + this.observerCounts.multipleDepChangeHandler++; + assert.equal(arguments.length, 3, 'observer argument length wrong'); + assert.equal(dep1, this.dep1, 'dependency 1 argument wrong'); + assert.equal(dep2, this.dep2, 'dependency 2 argument wrong'); + assert.equal(dep3, this.dep3, 'dependency 3 argument wrong'); + }, computeInline: function(value, add, divide) { + assert.equal(arguments.length, 3, 'observer argument length wrong'); + assert.equal(value, this.value, 'dependency 1 argument wrong'); + assert.equal(add, this.add, 'dependency 2 argument wrong'); + assert.equal(divide, this.divide, 'dependency 3 argument wrong'); return (value + add) / divide; }, - customEventValueChanged: function() {}, - customEventObjectValueChanged: function() {} + customEventValueChanged: function(val, old) { + this.observerCounts.customEventValueChanged++; + assert.equal(arguments.length, 2, 'observer argument length wrong'); + assert.equal(val, this.customEventValue, 'observer value argument wrong'); + assert.equal(old, this._customEventValue, 'observer old argument wrong'); + this._customEventValue = val; + }, + customEventObjectValueChanged: function(val) { + this.observerCounts.customEventObjectValueChanged++; + assert.equal(arguments.length, 1, 'observer argument length wrong'); + assert.equal(val, this.customEventObject.value, 'observer value argument wrong'); + // note, no `old` argument for path observers + } }); @@ -121,18 +200,42 @@ diff --git a/test/unit/bind.html b/test/unit/bind.html index c2597c783a..940ceefc2d 100644 --- a/test/unit/bind.html +++ b/test/unit/bind.html @@ -49,33 +49,24 @@ assert.equal(el.$.boundChild.negvalue, true, 'Value not negated'); }); - test('change handler called', function() { - var called = false; - el.valueChanged = function() { - called = true; - }; + test('observer called', function() { + assert.equal(el.observerCounts.valueChanged, 1, 'observer not called once for default value at configure'); el.value = 43; - assert.equal(called, true, 'Change handler not called'); + assert.equal(el.observerCounts.valueChanged, 2, 'observer not called after property change'); }); - test('computed value updates', function(done) { + test('computed value updates', function() { el.value = 44; - 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(); - }); + assert.equal(el.computedvalue, 45, 'Computed value not correct'); + assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child'); }); - test('computed values to same method updates', function(done) { + test('computed values to same method updates', function() { el.value = 44; el.valuetwo = 144; - setTimeout(function() { - assert.equal(el.computedvalue, 45, 'Computed value not correct'); - assert.equal(el.computedvaluetwo, 145, 'Computed value not correct'); - assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child'); - done(); - }); + assert.equal(el.computedvalue, 45, 'Computed value not correct'); + assert.equal(el.computedvaluetwo, 145, 'Computed value not correct'); + assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child'); }); test('notification sent', function() { @@ -93,126 +84,85 @@ assert.equal(notified, 2, 'Notification events not sent'); }); - test('computed change handler called', function(done) { - var called = false; - el.computedvalueChanged = function() { - called = true; - }; + test('computed observer called', function() { + el.clearObserverCounts(); el.value = 46; - setTimeout(function() { - assert.equal(called, true, 'Change handler not called'); - done(); - }); + assert.equal(el.observerCounts.computedvalueChanged, 1, 'observer not called'); }); - test('computed notification sent', function(done) { - var notified = false; + test('computed notification sent', function() { + var notified = 0; el.addEventListener('computednotifyingvalue-changed', function(e) { assert.equal(e.detail.value, 49); - notified = true; + notified++; }); el.notifyingvalue = 47; - setTimeout(function() { - assert.equal(notified, true, 'Notification event not sent'); - done(); - }); + assert.equal(notified, 1, 'Notification event not sent'); }); - test('computed property with multiple dependencies', function(done) { - var called = 0; - el.computedFromMultipleValuesChanged = function() { - called++; - }; - var notified = false; + test('computed property with multiple dependencies', function() { + var notified = 0; el.addEventListener('computed-from-multiple-values-changed', function(e) { - notified = true; + notified++; }); 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, 1, 'Change handler not called'); - done(); - }); + assert.equal(el.computedFromMultipleValues, 15, 'Computed value wrong'); + assert.equal(notified, 1, 'Notification event not sent'); + assert.equal(el.observerCounts.computedFromMultipleValuesChanged, 1, 'observer not called'); }); - test('no read-only change handler called with assignment', function() { - var called = false; - el.readonlyvalueChanged = function() { - called = true; - }; + test('no read-only observer called with assignment', function() { el.readolyvalue = 46; - assert.equal(called, false, 'Change handler should not be called for readOnly prop assignment'); + assert.equal(el.observerCounts.readonlyvalueChanged, 0, 'observer should not be called for readOnly prop assignment'); }); - test('read-only change handler called with _setReadonlyvalue', function() { - var called = false; - el.readonlyvalueChanged = function() { - assert(el.readonlyvalue == 46, 'value should be changed but was not'); - called = true; - }; + test('read-only observer called with _setReadonlyvalue', function() { el._setReadonlyvalue(46); - assert.equal(called, true, 'Change handler should be called'); + assert.equal(el.observerCounts.readonlyvalueChanged, 1, 'observer should be called'); assert(el.readonlyvalue == 46, 'value should be changed but was not'); }); test('no read-only notification sent with assignment', function() { - var notified = false; + var notified = 0; el.addEventListener('readonlyvalue-changed', function(e) { - notified = true; + notified++; }); el.readonlyvalue = 47; - assert.equal(notified, false, 'Notification should not be called for readOnly prop assignment'); + assert.equal(notified, 0, 'Notification should not be called for readOnly prop assignment'); }); test('read-only notification sent with _setReadonlyvalue', function() { - var notified = false; + var notified = 0; el.addEventListener('readonlyvalue-changed', function(e) { assert.equal(e.detail.value, 47); - notified = true; + notified++; }); el._setReadonlyvalue(47); - assert.equal(notified, true, 'Notification event not sent'); + assert.equal(notified, 1, 'Notification event not sent'); }); - test('multiple dependency change handler called once', function(done) { - var called = 0; - el.multipleDepChangeHandler = function(dep1, dep2, dep3) { - called++; - assert.equal(dep1, el.dep1, 'dependency 1 argument wrong'); - assert.equal(dep2, el.dep2, 'dependency 2 argument wrong'); - assert.equal(dep3, el.dep3, 'dependency 3 argument wrong'); - }; + test('multiple dependency observer called once', function() { el.dep1 = true; el.dep2 = {}; el.dep3 = 42; - setTimeout(function() { - assert.equal(called, 1, 'change handler not called once'); - done(); - }); + assert.equal(el.observerCounts.multipleDepChangeHandler, 1, 'observer not called once'); }); - test('annotated computed property', function(done) { + test('annotated computed property', function() { el.value = 20; el.add = 40; el.divide = 3; - setTimeout(function() { - assert.equal(el.$.boundChild.computedInline, 20, 'computedInline not correct'); - assert.equal(el.$.boundChild.computedInline2, 20, 'computedInline2 not correct'); - done(); - }); + assert.equal(el.$.boundChild.computedInline, 20, 'computedInline not correct'); + assert.equal(el.$.boundChild.computedInline2, 20, 'computedInline2 not correct'); }); - test('annotated computed attribute', function(done) { + test('annotated computed attribute', function() { el.value = 20; el.add = 40; el.divide = 3; - setTimeout(function() { - assert.equal(el.$.boundChild.getAttribute('computedattribute'), 20, 'computed attribute not correct'); - done(); - }); + assert.equal(el.$.boundChild.getAttribute('computedattribute'), 20, 'computed attribute not correct'); }); test('annotated style attribute binding', function() { @@ -227,21 +177,17 @@ }); test('custom notification event to property', function() { - var called = 0; - el.customEventValueChanged = function() { called++; }; el.$.boundChild.customEventValue = 42; el.fire('custom', null, el.$.boundChild); assert.equal(el.customEventValue, 42, 'custom bound property incorrect'); - assert.equal(called, 1, 'custom bound property observer not called'); + assert.equal(el.observerCounts.customEventValueChanged, 1, 'custom bound property observer not called'); }); test('custom notification event to path', function() { - var called = 0; - el.customEventObjectValueChanged = function() { called++; }; el.$.boundChild.customEventObjectValue = 84; el.fire('change', null, el.$.boundChild); assert.equal(el.customEventObject.value, 84, 'custom bound path incorrect'); - assert.equal(called, 1, 'custom bound path observer not called'); + assert.equal(el.observerCounts.customEventObjectValueChanged, 1, 'custom bound path observer not called'); }); }); @@ -270,34 +216,20 @@ assert.equal(el.boundvalue, 42, 'binding to non-notifying property updated and should not have been'); }); - test('changed handler for property bound to non-notifying property', function() { - var called = false; - el.boundvalueChanged = function() { - called = true; - }; + test('observer for property bound to non-notifying property', function() { el.$.basic1.value = 44; - assert.equal(called, false, 'changed handler for property bound to non-notifying property called and should not have been'); + assert.equal(el.observerCounts.boundvalueChanged, 0, 'observer for property bound to non-notifying property called and should not have been'); }); - test('binding to non-notifying computed property', function(done) { + test('binding to non-notifying computed property', function() { el.boundcomputedvalue = 42; el.$.basic1.value = 43; - setTimeout(function() { - assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been'); - done(); - }); + assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been'); }); - test('changed handler for property bound to non-notifying computed property', function(done) { - var called = false; - el.boundcomputedvalueChanged = function() { - called = true; - }; + test('observer for property bound to non-notifying computed property', function() { el.$.basic1.value = 44; - setTimeout(function() { - assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been'); - done(); - }); + assert.equal(el.observerCounts.boundcomputedvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been'); }); test('binding to notifying property', function() { @@ -310,55 +242,32 @@ assert.equal(el.boundnotifyingvalue, -43, 'camel-case binding to notifying property not updated'); }); - test('changed handler for property bound to notifying property', function() { - var called = false; - el.boundnotifyingvalueChanged = function() { - called = true; - }; + test('observer for property bound to notifying property', function() { el.$.basic1.notifyingvalue = 45; - assert.equal(called, true, 'changed handler for property bound to notifying property not called'); + assert.equal(el.observerCounts.boundnotifyingvalueChanged, 1, 'observer for property bound to notifying property not called'); }); - test('binding to notifying computed property', function(done) { + test('binding to notifying computed property', function() { el.$.basic1.notifyingvalue = 43; - setTimeout(function() { - assert.equal(el.boundcomputednotifyingvalue, 45, 'binding to notifying computed property not updated'); - done(); - }); + assert.equal(el.boundcomputednotifyingvalue, 45, 'binding to notifying computed property not updated'); }); - test('changed handler for property bound to notifying computed property', function(done) { - var called = false; - el.boundcomputednotifyingvalueChanged = function() { - called = true; - }; + test('observer for property bound to notifying computed property', function() { el.$.basic1.notifyingvalue = 45; - setTimeout(function() { - assert.equal(called, true, 'changed handler for property bound to non-notifying computed property not called'); - done(); - }); + assert.equal(el.observerCounts.boundcomputednotifyingvalueChanged, 1, 'observer for property bound to non-notifying computed property not called'); }); test('no change for binding into read-only property', function() { - var called = false; el.$.basic1._setReadonlyvalue(45); - el.$.basic1.readonlyvalueChanged = function() { - called = true; - }; + el.$.basic1.clearObserverCounts(); el.boundreadonlyvalue = 46; - assert.equal(called, false, 'change handler for read-only property should not be called from change to bound value'); + assert.equal(el.$.basic1.observerCounts.readonlyvalueChanged, 0, 'observer for read-only property should not be called from change to bound value'); assert.equal(el.$.basic1.readonlyvalue, 45, 'read-only property should not change from change to bound value'); }); test('change for binding out of read-only property', function() { - var called = false; - el.boundreadonlyvalueChanged = function(neo, old) { - assert.equal(el.boundreadonlyvalue, 46, 'property bound to read-only property should change from change to bound value'); - assert.equal(neo, 46, 'change handler argument incorrect'); - called = true; - }; el.$.basic1._setReadonlyvalue(46); - assert.equal(called, true, 'change handler for property bound to read-only property should be called from change to bound value'); + assert.equal(el.observerCounts.boundreadonlyvalueChanged, 1, 'observer for property bound to read-only property should be called from change to bound value'); assert.equal(el.boundreadonlyvalue, 46, 'property bound to read-only property should change from change to bound value'); }); @@ -384,34 +293,20 @@ assert.equal(el.boundvalue, 42, 'binding to non-notifying property updated and should not have been'); }); - test('changed handler for property one-way-bound to non-notifying property', function() { - var called = false; - el.boundvalueChanged = function() { - called = true; - }; + test('observer for property one-way-bound to non-notifying property', function() { el.$.basic2.value = 44; - assert.equal(called, false, 'changed handler for property one-way-bound to non-notifying property called and should not have been'); + assert.equal(el.observerCounts.boundvalueChanged, 0, 'observer 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(done) { + test('one-way binding to non-notifying computed property', function() { el.boundcomputedvalue = 42; el.$.basic2.value = 43; - setTimeout(function() { - assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been'); - done(); - }); + assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been'); }); - test('changed handler for property one-way-bound to non-notifying computed property', function(done) { - var called = false; - el.boundcomputedvalueChanged = function() { - called = true; - }; + test('observer for property one-way-bound to non-notifying computed property', function() { el.$.basic2.value = 44; - setTimeout(function() { - assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been'); - done(); - }); + assert.equal(el.observerCounts.boundcomputedvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been'); }); test('one-way binding to notifying property', function() { @@ -421,34 +316,20 @@ assert.equal(el.boundnotifyingvalue, 42, 'binding to notifying property updated and should not have been'); }); - test('changed handler for property one-way-bound to notifying property', function() { - var called = false; - el.boundnotifyingvalueChanged = function() { - called = true; - }; + test('observer for property one-way-bound to notifying property', function() { el.$.basic2.notifyingvalue = 45; - assert.equal(called, false, 'changed handler for property bound to notifying property called and should not have been'); + assert.equal(el.observerCounts.boundnotifyingvalueChanged, 0, 'observer for property bound to notifying property called and should not have been'); }); - test('one-way binding to notifying computed property', function(done) { + test('one-way binding to notifying computed property', function() { el.boundcomputednotifyingvalue = 42; el.$.basic2.notifyingvalue = 43; - setTimeout(function() { - assert.equal(el.boundcomputednotifyingvalue, 42, 'binding to notifying computed property updated and should not have been'); - done(); - }); + assert.equal(el.boundcomputednotifyingvalue, 42, 'binding to notifying computed property updated and should not have been'); }); - test('changed handler for property one-way-bound to notifying computed property', function(done) { - var called = false; - el.boundcomputednotifyingvalueChanged = function() { - called = true; - }; + test('observer for property one-way-bound to notifying computed property', function() { el.$.basic2.notifyingvalue = 45; - setTimeout(function() { - assert.equal(called, false, 'changed handler for property bound to non-notifying computed property called and should not have been'); - done(); - }); + assert.equal(el.observerCounts.boundcomputednotifyingvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been'); }); }); diff --git a/test/unit/notify-path-elements.html b/test/unit/notify-path-elements.html index 62423851b0..a8ae0cf363 100644 --- a/test/unit/notify-path-elements.html +++ b/test/unit/notify-path-elements.html @@ -24,12 +24,33 @@ notify: true } }, - observers: { - 'obj.*': 'objSubpathChanged', - 'obj.value': 'objValueChanged', + observers: [ + 'objSubpathChanged(obj.*)', + 'objValueChanged(obj.value)', + ], + created: function() { + this.observerCounts = { + objSubpathChanged: 0, + objValueChanged: 0 + }; + }, + clearObserverCounts: function() { + for (var i in this.observerCounts) { + this.observerCounts[i] = 0; + } + }, + objSubpathChanged: function(change) { + this.observerCounts.objSubpathChanged++; + assert.equal(change.base, this.obj); + if (this.expectedObjSubpath) { + assert.equal(change.path, this.expectedObjSubpath); + assert.equal(change.value, this.expectedObjValue); + } + }, + objValueChanged: function(value) { + this.observerCounts.objValueChanged++; + assert.equal(this.obj.value, value); }, - objSubpathChanged: function() {}, - objValueChanged: function() {}, }); @@ -45,12 +66,34 @@ notify: true } }, - observers: { - 'obj.*': 'objSubpathChanged', - 'obj.value': 'objValueChanged', + observers: [ + 'objSubpathChanged(obj.*)', + 'objValueChanged(obj.value)', + ], + created: function() { + this.observerCounts = { + objSubpathChanged: 0, + objValueChanged: 0 + }; + }, + clearObserverCounts: function() { + for (var i in this.observerCounts) { + this.observerCounts[i] = 0; + } + this.$.compose.clearObserverCounts(); + }, + objSubpathChanged: function(change) { + this.observerCounts.objSubpathChanged++; + assert.equal(change.base, this.obj); + if (this.expectedObjSubpath) { + assert.equal(change.path, this.expectedObjSubpath); + assert.equal(change.value, this.expectedObjValue); + } + }, + objValueChanged: function(value) { + this.observerCounts.objValueChanged++; + assert.equal(this.obj.value, value); }, - objSubpathChanged: function() {}, - objValueChanged: function() {}, }); @@ -58,19 +101,87 @@ +
diff --git a/test/unit/notify-path.html b/test/unit/notify-path.html index c2429a6c8b..53bf36fd24 100644 --- a/test/unit/notify-path.html +++ b/test/unit/notify-path.html @@ -33,29 +33,33 @@ }); test('downward data flow', function() { + // Setup var nested = { obj: { value: 42 } }; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.nestedObjChanged = function() { changed[1] = true; }; - el.objSubpathChanged = function() { changed[2] = true; }; - el.objValueChanged = function() { changed[3] = true; }; - el.$.compose.objSubpathChanged = function() { changed[4] = true; }; - el.$.compose.objValueChanged = function() { changed[5] = true; }; - el.$.forward.objSubpathChanged = function() { changed[6] = true; }; - el.$.forward.objValueChanged = function() { changed[7] = true; }; + el.expectedNestedSubpath = 'nested'; + el.expectedNestedValue = nested; + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = nested.obj; + el.$.compose.expectedObjSubpath = 'obj'; + el.$.compose.expectedObjValue = nested.obj; + el.$.forward.expectedObjSubpath = 'obj'; + el.$.forward.expectedObjValue = nested.obj; + el.$.forward.$.compose.expectedObjSubpath = 'obj'; + el.$.forward.$.compose.expectedObjValue = nested.obj; + // Do the thing el.nested = nested; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); - assert.equal(changed[7], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 1); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.obj, nested.obj); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); @@ -72,28 +76,35 @@ }); test('notification from basic element property change', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.$.basic.notifyingValue = 42; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -107,28 +118,36 @@ }); test('notification from composed element property change', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.$.compose.$.basic1.notifyingValue = 42; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -142,28 +161,36 @@ }); test('notification from forward\'s composed element property change', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.$.forward.$.compose.$.basic1.notifyingValue = 42; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -177,28 +204,36 @@ }); test('notification from setPathValue in top element', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.setPathValue('nested.obj.value', 42); - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -212,28 +247,36 @@ }); test('notification from setPathValue in composed element', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.$.compose.setPathValue('obj.value', 42); - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -247,28 +290,36 @@ }); test('notification from setPathValue in forward element', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.$.forward.setPathValue('obj.value', 42); - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -282,28 +333,36 @@ }); test('notification from setPathValue in forward\'s composed element', function() { + // Setup var nested = { obj: { value: 41 } }; el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.objSubpathChanged = function() { changed[1] = true; }; - el.objValueChanged = function() { changed[2] = true; }; - el.$.compose.objSubpathChanged = function() { changed[3] = true; }; - el.$.compose.objValueChanged = function() { changed[4] = true; }; - el.$.forward.objSubpathChanged = function() { changed[5] = true; }; - el.$.forward.objValueChanged = function() { changed[6] = true; }; + el.expectedNestedSubpath = 'nested.obj.value'; + el.expectedNestedValue = 42; + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 42; + el.$.compose.expectedObjSubpath = 'obj.value'; + el.$.compose.expectedObjValue = 42; + el.$.forward.expectedObjSubpath = 'obj.value'; + el.$.forward.expectedObjValue = 42; + el.$.forward.$.compose.expectedObjSubpath = 'obj.value'; + el.$.forward.$.compose.expectedObjValue = 42; + el.clearObserverCounts(); + // Do the thing el.$.forward.$.compose.setPathValue('obj.value', 42); - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 0); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -317,32 +376,38 @@ }); test('notification from object change in compose element', function() { - var nested = { + // Setup + el.nested = { obj: { value: 41 } }; - el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.nestedObjChanged = function() { changed[1] = true; }; - el.objSubpathChanged = function() { changed[2] = true; }; - el.objValueChanged = function() { changed[3] = true; }; - el.$.compose.objSubpathChanged = function() { changed[4] = true; }; - el.$.compose.objValueChanged = function() { changed[5] = true; }; - el.$.forward.objSubpathChanged = function() { changed[6] = true; }; - el.$.forward.objValueChanged = function() { changed[7] = true; }; - el.$.compose.obj = { + var obj = { value: 42 }; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); - assert.equal(changed[7], true); + el.expectedNestedSubpath = 'nested.obj'; + el.expectedNestedValue = obj; + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = obj; + el.$.compose.expectedObjSubpath = 'obj'; + el.$.compose.expectedObjValue = obj; + el.$.forward.expectedObjSubpath = 'obj'; + el.$.forward.expectedObjValue = obj; + el.$.forward.$.compose.expectedObjSubpath = 'obj'; + el.$.forward.$.compose.expectedObjValue = obj; + el.clearObserverCounts(); + // Do the thing + el.$.compose.obj = obj; + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 1); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -356,32 +421,38 @@ }); test('notification from object change in forward element', function() { - var nested = { + // Setup + el.nested = { obj: { value: 41 } }; - el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.nestedObjChanged = function() { changed[1] = true; }; - el.objSubpathChanged = function() { changed[2] = true; }; - el.objValueChanged = function() { changed[3] = true; }; - el.$.compose.objSubpathChanged = function() { changed[4] = true; }; - el.$.compose.objValueChanged = function() { changed[5] = true; }; - el.$.forward.objSubpathChanged = function() { changed[6] = true; }; - el.$.forward.objValueChanged = function() { changed[7] = true; }; - el.$.forward.obj = { + var obj = { value: 42 }; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); - assert.equal(changed[7], true); + el.expectedNestedSubpath = 'nested.obj'; + el.expectedNestedValue = obj; + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = obj; + el.$.compose.expectedObjSubpath = 'obj'; + el.$.compose.expectedObjValue = obj; + el.$.forward.expectedObjSubpath = 'obj'; + el.$.forward.expectedObjValue = obj; + el.$.forward.$.compose.expectedObjSubpath = 'obj'; + el.$.forward.$.compose.expectedObjValue = obj; + el.clearObserverCounts(); + // Do the thing + el.$.forward.obj = obj; + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 1); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -395,32 +466,38 @@ }); test('notification from object change in forward\'s compose element', function() { - var nested = { + // Setup + el.nested = { obj: { value: 41 } }; - el.nested = nested; - var changed = []; - el.nestedSubpathChanged = function() { changed[0] = true; }; - el.nestedObjChanged = function() { changed[1] = true; }; - el.objSubpathChanged = function() { changed[2] = true; }; - el.objValueChanged = function() { changed[3] = true; }; - el.$.compose.objSubpathChanged = function() { changed[4] = true; }; - el.$.compose.objValueChanged = function() { changed[5] = true; }; - el.$.forward.objSubpathChanged = function() { changed[6] = true; }; - el.$.forward.objValueChanged = function() { changed[7] = true; }; - el.$.forward.$.compose.obj = { + var obj = { value: 42 }; - assert.equal(changed[0], true); - assert.equal(changed[1], true); - assert.equal(changed[2], true); - assert.equal(changed[3], true); - assert.equal(changed[4], true); - assert.equal(changed[5], true); - assert.equal(changed[6], true); - assert.equal(changed[7], true); + el.expectedNestedSubpath = 'nested.obj'; + el.expectedNestedValue = obj; + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = obj; + el.$.compose.expectedObjSubpath = 'obj'; + el.$.compose.expectedObjValue = obj; + el.$.forward.expectedObjSubpath = 'obj'; + el.$.forward.expectedObjValue = obj; + el.$.forward.$.compose.expectedObjSubpath = 'obj'; + el.$.forward.$.compose.expectedObjValue = obj; + el.clearObserverCounts(); + // Do the thing + el.$.forward.$.compose.obj = obj; + // Verify + assert.equal(el.observerCounts.nestedSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjChanged, 1); + assert.equal(el.observerCounts.nestedObjSubpathChanged, 1); + assert.equal(el.observerCounts.nestedObjValueChanged, 1); + assert.equal(el.$.compose.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.compose.observerCounts.objValueChanged, 1); + assert.equal(el.$.forward.observerCounts.objSubpathChanged, 1); + assert.equal(el.$.forward.observerCounts.objValueChanged, 1); + assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.basic.notifyingValue, 42); assert.equal(el.$.compose.$.basic1.notifyingValue, 42); assert.equal(el.$.compose.$.basic2.notifyingValue, 42); @@ -434,12 +511,15 @@ }); test('negation', function() { + // Setup var nested = { obj: { value: false } }; + // Do the thing el.nested = nested; + // Verify assert.equal(el.$.basic.notifyingValue, false); assert.equal(el.$.compose.$.basic1.notifyingValue, false); assert.equal(el.$.compose.$.basic2.notifyingValue, false); @@ -455,7 +535,9 @@ assert.equal(el.$.forward.$.compose.$.basic2.hasAttribute('attrvalue'), false); assert.equal(el.$.forward.$.compose.$.basic3.hasAttribute('attrvalue'), true); + // Do another thing el.$.basic.notifyingValue = true; + // Verify assert.equal(el.$.basic.notifyingValue, true); assert.equal(el.$.compose.$.basic1.notifyingValue, true); assert.equal(el.$.compose.$.basic2.notifyingValue, true); @@ -471,7 +553,9 @@ assert.equal(el.$.forward.$.compose.$.basic2.hasAttribute('attrvalue'), true); assert.equal(el.$.forward.$.compose.$.basic3.hasAttribute('attrvalue'), false); + // Do another thing el.$.forward.$.compose.$.basic1.notifyingValue = false; + // Verify assert.equal(el.$.basic.notifyingValue, false); assert.equal(el.$.compose.$.basic1.notifyingValue, false); assert.equal(el.$.compose.$.basic2.notifyingValue, false); @@ -487,7 +571,9 @@ assert.equal(el.$.forward.$.compose.$.basic2.hasAttribute('attrvalue'), false); assert.equal(el.$.forward.$.compose.$.basic3.hasAttribute('attrvalue'), true); + // Do another thing el.setPathValue('nested.obj.value', true); + // Verify assert.equal(el.$.basic.notifyingValue, true); assert.equal(el.$.compose.$.basic1.notifyingValue, true); assert.equal(el.$.compose.$.basic2.notifyingValue, true); @@ -503,8 +589,10 @@ assert.equal(el.$.forward.$.compose.$.basic2.hasAttribute('attrvalue'), true); assert.equal(el.$.forward.$.compose.$.basic3.hasAttribute('attrvalue'), false); + // Do another thing // no two way binding through negation el.$.compose.$.basic3.notifyingValue = true; + // Verify assert.equal(el.$.basic.notifyingValue, true); assert.equal(el.$.compose.$.basic1.notifyingValue, true); assert.equal(el.$.compose.$.basic2.notifyingValue, true); @@ -540,6 +628,115 @@ assert.equal(el.$.compose.$.basic1.othervalue, 98); }); +}); + +suite('path effects', function() { + + var el; + + setup(function() { + el = document.createElement('x-stuff'); + document.body.appendChild(el); + }); + + teardown(function() { + document.body.removeChild(el); + }); + + test('observer with multiple args, path last', function() { + // Setup + var nested = { + obj: { + value: 42 + } + }; + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = nested.obj; + // Do the thing + el.a = 'a'; + el.b = 'b'; + el.nested = nested; + // Verify + assert.equal(el.observerCounts.multipleChanged, 1); + }); + + test('observer with multiple args, path not last', function() { + // Setup + var nested = { + obj: { + value: 42 + } + }; + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = nested.obj; + // Do the thing + el.a = 'a'; + el.nested = nested; + el.b = 'b'; + // Verify + assert.equal(el.observerCounts.multipleChanged, 1); + }); + + test('observer with multiple args, path first, then last', function() { + // Setup + var nested = { + obj: { + value: 42 + } + }; + el.nested = nested; + el.clearObserverCounts(); + el.expectedNestedObjSubpath = 'nested.obj'; + el.expectedNestedObjValue = nested.obj; + // Do the thing + el.a = 'a'; + el.b = 'b'; + // Verify + assert.equal(el.observerCounts.multipleChanged, 1); + + // Setup + el.expectedNestedObjSubpath = 'nested.obj.value'; + el.expectedNestedObjValue = 43; + // Do another thing + el.setPathValue('nested.obj.value', 43); + // Verify + assert.equal(el.observerCounts.multipleChanged, 2); + }); + + test('observer & computed with multiple path args', function() { + // Setup + var nested = { + b: 33, + obj: { + c: 66 + } + }; + // Do the thing + el.a = 1; + el.nested = nested; + // Verify + // Multiple-dependency observers with dependencies on the same + // object will be called once for each dependency on the shared object + assert.equal(el.observerCounts.multiplePathsChanged, 2); + assert.equal(el.computedFromPaths, 100); + assert.equal(el.$.boundChild.computedFromPaths, 100); + }); + +}); + +suite('path API', function() { + + var el; + + setup(function() { + el = document.createElement('x-stuff'); + document.body.appendChild(el); + }); + + teardown(function() { + document.body.removeChild(el); + }); + test('getPathValue', function() { el.simple = 11; el.nested = { diff --git a/test/unit/x-repeat-elements.html b/test/unit/x-repeat-elements.html new file mode 100644 index 0000000000..6ac0c2182f --- /dev/null +++ b/test/unit/x-repeat-elements.html @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + diff --git a/test/unit/x-repeat.html b/test/unit/x-repeat.html new file mode 100644 index 0000000000..785c907deb --- /dev/null +++ b/test/unit/x-repeat.html @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + +