Skip to content

Commit

Permalink
Factor PropertiesChanged out of PropertyAccessors
Browse files Browse the repository at this point in the history
To improve code hygiene and extensibility, factors the basic batched `_propertiesChanged` mechanism out of `PropertyAccessors` which deals with creating accessors and sync'ing with attributes.
  • Loading branch information
Steven Orvell committed Sep 15, 2017
1 parent c7b43f7 commit aa4f186
Show file tree
Hide file tree
Showing 3 changed files with 412 additions and 151 deletions.
252 changes: 252 additions & 0 deletions lib/mixins/properties-changed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="../utils/boot.html">
<link rel="import" href="../utils/mixin.html">
<link rel="import" href="../utils/async.html">

<script>
(function() {

'use strict';

let microtask = Polymer.Async.microTask;

/**
* Element class mixin that provides basic meta-programming for creating one
* or more property accessors (getter/setter pair) that enqueue an async
* (batched) `_propertiesChanged` callback.
*
* For basic usage of this mixin, simply declare attributes to observe via
* the standard `static get observedAttributes()`, implement `_propertiesChanged`
* on the class, and then call `MyClass.createPropertiesForAttributes()` once
* on the class to generate property accessors for each observed attribute
* prior to instancing. Last, call `this._enableProperties()` in the element's
* `connectedCallback` to enable the accessors.
*
* Any `observedAttributes` will automatically be
* deserialized via `attributeChangedCallback` and set to the associated
* property using `dash-case`-to-`camelCase` convention.
*
* @mixinFunction
* @polymer
* @memberof Polymer
* @summary Element class mixin for reacting to property changes from
* generated property accessors.
*/
Polymer.PropertiesChanged = Polymer.dedupingMixin(superClass => {

return class PropertiesChanged extends superClass {

constructor() {
super();
this.__dataEnabled = false;
this.__dataReady = false;
this.__dataInvalid = false;
this.__data = {};
this.__dataPending = null;
this.__dataOld = null;
this.__dataInstanceProps = null;
this._initializeProperties();
}

/**
* Lifecycle callback called when properties are enabled via
* `_enableProperties`.
*
* Users may override this function to implement behavior that is
* dependent on the element having its property data initialized, e.g.
* from defaults (initialized from `constructor`, `_initializeProperties`),
* `attributeChangedCallback`, or values propagated from host e.g. via
* bindings. `super.ready()` must be called to ensure the data system
* becomes enabled.
*
* @public
*/
ready() {
this.__dataReady = true;
}

/**
* Provided as an override point for performing any setup work prior
* to initializing the property accessor system.
*
* @protected
*/
_initializeProperties() {}

/**
* Called at ready time with bag of instance properties that overwrote
* accessors when the element upgraded.
*
* The default implementation sets these properties back into the
* setter at ready time. This method is provided as an override
* point for customizing or providing more efficient initialization.
*
* @param {Object} props Bag of property values that were overwritten
* when creating property accessors.
* @protected
*/
_initializeInstanceProperties(props) {
Object.assign(this, props);
}

/**
* Updates the local storage for a property (via `_setPendingProperty`)
* and enqueues a `_proeprtiesChanged` callback.
*
* @param {string} property Name of the property
* @param {*} value Value to set
* @protected
*/
_setProperty(property, value) {
if (this._setPendingProperty(property, value)) {
this._invalidateProperties();
}
}

_getProperty(property) {
return this.__data[property];
}

/**
* Updates the local storage for a property, records the previous value,
* and adds it to the set of "pending changes" that will be passed to the
* `_propertiesChanged` callback. This method does not enqueue the
* `_propertiesChanged` callback.
*
* @param {string} property Name of the property
* @param {*} value Value to set
* @return {boolean} Returns true if the property changed
* @protected
*/
_setPendingProperty(property, value) {
let old = this.__data[property];
let changed = this._shouldPropertyChange(property, value, old)
if (changed) {
if (!this.__dataPending) {
this.__dataPending = {};
this.__dataOld = {};
}
// Ensure old is captured from the last turn
if (this.__dataOld && !(property in this.__dataOld)) {
this.__dataOld[property] = old;
}
this.__data[property] = value;
this.__dataPending[property] = value;
}
return changed;
}

/**
* Marks the properties as invalid, and enqueues an async
* `_propertiesChanged` callback.
*
* @protected
*/
_invalidateProperties() {
if (!this.__dataInvalid && this.__dataReady) {
this.__dataInvalid = true;
microtask.run(() => this._validateProperties());
}
}

_validateProperties() {
if (this.__dataInvalid) {
this.__dataInvalid = false;
this._flushProperties();
}
}

/**
* Call to enable property accessor processing. Before this method is
* called accessor values will be set but side effects are
* queued. When called, any pending side effects occur immediately.
* For elements, generally `connectedCallback` is a normal spot to do so.
* It is safe to call this method multiple times as it only turns on
* property accessors once.
*/
_enableProperties() {
if (!this.__dataEnabled) {
this.__dataEnabled = true;
if (this.__dataInstanceProps) {
this._initializeInstanceProperties(this.__dataInstanceProps);
this.__dataInstanceProps = null;
}
this.ready()
}
}

/**
* Calls the `_propertiesChanged` callback with the current set of
* pending changes (and old values recorded when pending changes were
* set), and resets the pending set of changes. Generally, this method
* should not be called in user code.
*
*
* @protected
*/
_flushProperties() {
let changedProps = this.__dataPending;
this.__dataPending = null;
this._propertiesChanged(this.__data, changedProps, this.__dataOld);
}

/**
* Callback called when any properties with accessors created via
* `_createPropertyAccessor` have been set.
*
* @param {!Object} currentProps Bag of all current accessor values
* @param {!Object} changedProps Bag of properties changed since the last
* call to `_propertiesChanged`
* @param {!Object} oldProps Bag of previous values for each property
* in `changedProps`
* @protected
*/
_propertiesChanged(currentProps, changedProps, oldProps) { // eslint-disable-line no-unused-vars
}

/**
* Method called to determine whether a property value should be
* considered as a change and cause the `_propertiesChanged` callback
* to be enqueued.
*
* The default implementation returns `true` for primitive types if a
* strict equality check fails, and returns `true` for all Object/Arrays.
* The method always returns false for `NaN`.
*
* Override this method to e.g. provide stricter checking for
* Objects/Arrays when using immutable patterns.
*
* @param {string} property Property name
* @param {*} value New property value
* @param {*} old Previous property value
* @return {boolean} Whether the property should be considered a change
* and enqueue a `_proeprtiesChanged` callback
* @protected
*/
_shouldPropertyChange(property, value, old) {
return (
// Strict equality check
(old !== value &&
// This ensures (old==NaN, value==NaN) always returns false
(old === old || value === value))
);
}

}

return PropertiesChanged;

});


})();
</script>
Loading

0 comments on commit aa4f186

Please sign in to comment.