/** * @file mixins/stateful.js * @module stateful */ import {isEvented} from './evented'; import * as Obj from '../utils/obj'; /** * Contains methods that provide statefulness to an object which is passed * to {@link module:stateful}. * * @mixin StatefulMixin */ const StatefulMixin = { /** * A hash containing arbitrary keys and values representing the state of * the object. * * @type {Object} */ state: {}, /** * Set the state of an object by mutating its * {@link module:stateful~StatefulMixin.state|state} object in place. * * @fires module:stateful~StatefulMixin#statechanged * @param {Object|Function} stateUpdates * A new set of properties to shallow-merge into the plugin state. * Can be a plain object or a function returning a plain object. * * @return {Object|undefined} * An object containing changes that occurred. If no changes * occurred, returns `undefined`. */ setState(stateUpdates) { // Support providing the `stateUpdates` state as a function. if (typeof stateUpdates === 'function') { stateUpdates = stateUpdates(); } let changes; Obj.each(stateUpdates, (value, key) => { // Record the change if the value is different from what's in the // current state. if (this.state[key] !== value) { changes = changes || {}; changes[key] = { from: this.state[key], to: value }; } this.state[key] = value; }); // Only trigger "statechange" if there were changes AND we have a trigger // function. This allows us to not require that the target object be an // evented object. if (changes && isEvented(this)) { /** * An event triggered on an object that is both * {@link module:stateful|stateful} and {@link module:evented|evented} * indicating that its state has changed. * * @event module:stateful~StatefulMixin#statechanged * @type {Object} * @property {Object} changes * A hash containing the properties that were changed and * the values they were changed `from` and `to`. */ this.trigger({ changes, type: 'statechanged' }); } return changes; } }; /** * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target * object. * * If the target object is {@link module:evented|evented} and has a * `handleStateChanged` method, that method will be automatically bound to the * `statechanged` event on itself. * * @param {Object} target * The object to be made stateful. * * @param {Object} [defaultState] * A default set of properties to populate the newly-stateful object's * `state` property. * * @return {Object} * Returns the `target`. */ function stateful(target, defaultState) { Object.assign(target, StatefulMixin); // This happens after the mixing-in because we need to replace the `state` // added in that step. target.state = Object.assign({}, target.state, defaultState); // Auto-bind the `handleStateChanged` method of the target object if it exists. if (typeof target.handleStateChanged === 'function' && isEvented(target)) { target.on('statechanged', target.handleStateChanged); } return target; } export default stateful;