/** * StateModel reduces the complexibility of making the UI and a module state * synchronized while keeping the UI responsive at all times. A typical use case * would be bound a toggle to a hardware feature that requires time for turning * on and off. * * StateModel was designed for the following goals: * - Immediate response * The UI is not blocked even the target module can not respond to the user * request immediately. * - Displaying the target state * The final state should always be the last state requested from users unless * the module fails to fufill the request. * - Displaying the real state * If the module fails to be set to the target state or it changes the state * by itself, the final state is the real module state. * * StateModel needs to be created with an object used for performing the * state change operations. The object should define the following three * functinos: * - onInit * It is called only once when the model is being initialized. The function * takes a function as the parameter used for reporting the state changes * from the module. It can be omitted if your module won't change the state * by itself. onInit should also return an initial state immediately. * * - onSetState * It is called with a target state. Do the operation required for switching * to the target state. Always return a promise resolved/rejected with a * state. * If the promise resolves with a state that does not equal to the * latest target state, StateModel will call to this function again with the * target state. If the promise rejects, StateModel simply changes the * target state to the reported state because the module fails to be changed * to the desired state. * * - onGetState * It is called for requesting the current state. Return a promise wrapping * the current state. StateModel fails to be initialized if the returning * promise rejects. * * StateModel provides three observable properties: * - targetState * This is a property representing the user desired state and it gets updated * to the real module state when required. Binding this property to the UI * should be sufficient for most of the use cases. * - currentState * This is a readonly property reporting the latest stable state. * - transitioning * This is a readonly property showing if it is in transition between states. * * @example * var stateModel = StateModel({ * _state: null, * onInit: function(stateChange) { * return this._state; * }, * onSetState: function(state) { * // A fake time consuming operation * return new Promise((resolve) => { * setTimeout(() => { * this._state = state; * resolve(this._state); * }, 10000); * }); * }, * onGetState: function() { * return Promise.resolve(this._state); * } * }); * * // Do the binding * checkbox.addEventListener('change', function() { * stateModel.targetState = checkbox.checked; * }); * stateModel.observe('targetState', function(newState) { * checkbox.checked = newState; * }); */ define(function(require) { 'use strict'; var Module = require('modules/base/module'); var Observable = require('modules/mvvm/observable'); /** * @class StateModel * @param {Function} onInit * @param {Function} onSetState * @param {Function} onGetState * @returns {StateModel} */ var StateModel = Module.create( function StateModel({onInit, onGetState, onSetState}) { if (typeof onInit !== 'function' || typeof onGetState !== 'function' || typeof onSetState !== 'function') { this.throw('invalid arguments'); } this.super(Observable).call(this); var argument = arguments[0]; this._init(onInit.bind(argument), onGetState.bind(argument), onSetState.bind(argument)); }).extend(Observable); /** * An observable property indicating the desired state. * * @access public * @memberOf StateModel.prototype * @type {Object} */ Observable.defineObservableProperty(StateModel.prototype, 'targetState', { value: null }); /** * An observable property indicating the current state. * * @access public * @readonly * @memberOf StateModel.prototype * @type {Object} */ Observable.defineObservableProperty(StateModel.prototype, 'currentState', { readonly: true, value: null }); /** * An observable property indicating if it is in transition. * * @access public * @readonly * @memberOf StateModel.prototype * @type {Boolean} */ Observable.defineObservableProperty(StateModel.prototype, 'transitioning', { readonly: true, value: false }); StateModel.prototype._init = function(onInit, onGetState, onSetState) { this._onInit = onInit; this._onGetState = onGetState; this._onSetState = onSetState; this.targetState = this._onInit((realState) => { if (!this._transitioning) { this._currentState = realState; this.targetState = realState; } }); this._currentState = this.targetState; // Do the initialization this._onGetState().then((realState) => { this._currentState = realState; if (this.targetState !== realState) { // If the current target state is not the real state, do set again. this._setState(this.targetState); } this.observe('targetState', (newState) => { if (this._transitioning || newState === this._currentState) { return; } this._setState(newState); }); }).catch((err) => { this.error('initialization error ' + err); }); }; StateModel.prototype._setState = function(state) { this.debug('set state: ' + state); this._transitioning = true; return this._onSetState(state).then((realState) => { this.debug('set state completed: ' + state + ', real state: ' + realState); this._currentState = realState; if (this.targetState !== realState) { // If the current target state is not the real state, do set again. this._setState(this.targetState); } else { this._transitioning = false; } }, (realState) => { this.debug('set state failed: ' + state + ', real state: ' + realState); this._currentState = realState; this.targetState = realState; this._transitioning = false; }); }; return StateModel; });