Home Reference Source

lib/Store.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Store = void 0;
/**
 * Created by sam_g on 2/7/2017.
 */
var _ = require("lodash");
// import * as areEqual from 'fbjs/lib/areEqual';s
/** Simple data store with history recording and on change callbacks */
var Store = /** @class */ (function () {
    /***
     * Creates a store and sets its initial state to the optional value passed in
     * @param state
     */
    function Store(state) {
        /***
         *
         * @ignore
         * @type {any[]}
         * @private
         */
        this._boundStates = [];
        this.maxHistoryLength = 32;
        /** @private {function[]} */
        this.onChangeCallbacks = {
            __anyChange: []
        };
        /** @property {object} [state={}] state - An object that represents the present state */
        this.state = state || {};
        /** @private */ // @ts-ignore
        this.state.__time = Date.now();
        /** @private An array of states from the first to the last, excluding the present state */
        this.stateHistory = [];
    }
    /**
     * Get an array of states from the first to the last
     * @return {object[]} An array of states
     */
    Store.prototype.getFullHistory = function () {
        return _.concat(this.stateHistory, [this.state]);
    };
    /**
     * Trims the history down to the last 'x' number of entries
     * @param {number} length - max number of state history entries
     */
    Store.prototype.trimHistory = function (length) {
        if (length === void 0) { length = 32; }
        this.stateHistory = this.stateHistory.slice(-length);
    };
    /***
     * Automaticly updates the state of stateParent
     * @example <caption>Binding to a React component</caption>
     * componentDidMount(){
     *     // Don't forget to unbind on unmount
     *     mainStore.bindState(this);
     * }
     * @param stateParent
     * @param {function} mapping - (string)=>string
     * @param {boolean} forward
     */
    Store.prototype.bindState = function (stateParent, mapping, forward) {
        if (forward === void 0) { forward = true; }
        this._boundStates.push({ boundState: stateParent, mapper: mapping });
        if (forward)
            this.forwardState(stateParent);
    };
    /***
     * Unbinds the state of stateParent
     * @example <caption>Unbinding to a React component</caption>
     * componentWillUnmount(){
     *     mainStore.unbindState(this);
     * }
     * @param stateParent
     */
    Store.prototype.unbindState = function (stateParent) {
        var index = -1;
        var boundStates = this._boundStates;
        for (var i = 0; i < boundStates.length; i++) {
            if (boundStates[i].boundState === stateParent)
                index = i;
        }
        if (index > -1) {
            boundStates.splice(index, 1);
        }
    };
    Store.prototype.forwardState = function (receivingStateParent, mapping) {
        var _this = this;
        // the new state that will be transferred to the receiving state
        var newState = {};
        var receivingKeys = Object.keys(receivingStateParent.state);
        // Loop through all of the values in this store's state
        Object.keys(this.state).forEach(function (key) {
            // Map the sender's key to the receiver
            var mappedKey = mapping ? mapping(key) : key;
            // If the mappedKey can be found in the receiver add the value to the new state
            if (receivingKeys.indexOf(mappedKey) != -1)
                newState[mappedKey] = _this.state[key];
        });
        // Send the new state to the receiving store
        receivingStateParent.setState(newState);
    };
    Store.prototype.forwardToBoundStates = function () {
        var _this = this;
        this._boundStates.forEach(function (info) { return _this.forwardState(info.boundState, info.mapper); });
    };
    /**
     * Get the previous state of the store
     * @return {object} previous state
     */
    Store.prototype.getPreviousState = function () {
        var index = this.stateHistory.length - 1;
        return this.stateHistory[index];
    };
    /**
     * Set the store's state and optionally save the last state to history
     * @param {object} newState
     * @param {boolean} historyMode
     * @example <caption>Set state and record previous state to history</caption>
     * myStore.setState({userName: "John Doe"});
     * @example <caption>Set state and forget about recording previous state to history</caption>
     * myStore.setState({userName: "Billy Bob", false});
     */
    Store.prototype.setState = function (newState, historyMode) {
        var _this = this;
        if (historyMode === void 0) { historyMode = true; }
        // Save the old state
        var oldState = this.state;
        // Update the state
        this.state = _.assign({}, oldState, newState);
        var changes = Store.getChangeList(oldState, this.state);
        this.trimHistory(this.maxHistoryLength);
        // Loop through list of changes and trigger their respective callbacks
        changes.forEach(function (o) {
            // Get the name of the changed property
            var property = o.property;
            // Get the new and old values of the property
            var newProperty = _this.state[property];
            var oldProperty = oldState[property];
            // Trigger the property's callback
            _this._triggerChangeCallbacks(property, newProperty, oldProperty);
        });
        if (changes.length) {
            // @ts-ignore
            this.state.__time = Date.now();
            if (historyMode == true) {
                this.stateHistory.push(oldState);
            }
            this._triggerChangeCallbacks("__anyChange", this.state, oldState);
            this.forwardToBoundStates();
        }
    };
    /**
     * Subscribe to changes in the state
     * @param {function(newValue: *, oldValue: *)} callback
     * @param {string} property - Name of the property to watch
     * @example <caption>Trigger callback when userName is updated</caption>
     * myStore.subscribeToChanges((newVal)=>{ console.log("userName changed to...", newVal)}, "userName");
     */
    Store.prototype.subscribeToChanges = function (callback, property) {
        if (property === void 0) { property = "__anyChange"; }
        if (this.onChangeCallbacks[property] == undefined)
            this.onChangeCallbacks[property] = [];
        if (typeof callback != "function") {
            console.error("Callback given is not a function as expected");
        }
        else {
            this.onChangeCallbacks[property].push(callback);
        }
    };
    /**
     * Remove subscription to changes in the state
     * @param {function} callback
     * @param {string} property - Name of the property to unsubscribe from
     */
    Store.prototype.unSubscribeToChanges = function (callback, property) {
        if (property === void 0) { property = "__anyChange"; }
        var callbackList = this.onChangeCallbacks[property];
        if (callbackList == undefined)
            return;
        this.onChangeCallbacks[property] = _.pull(callbackList, callback);
    };
    /**
     *
     * @param {object} oldState
     * @param {object} newState
     * @return {Array}
     */
    Store.getChangeList = function (oldState, newState) {
        // Loop through state properties looking for changes
        var changes = [];
        for (var property in newState) {
            var newProperty = newState[property];
            var oldProperty = oldState[property];
            if (property == "__time")
                continue;
            // Generate change list
            if (!_.isEqual(newProperty, oldProperty)) {
                changes.push({
                    property: property,
                    isNew: oldProperty == undefined || oldProperty == null
                });
            }
        }
        return changes;
    };
    /**
     * Returns difference between two states in human-readable form
     * @param {object} oldState
     * @param {object} newState
     * @return {string}
     */
    Store.getChangeLog = function (oldState, newState) {
        var changes = this.getChangeList(oldState, newState);
        var log = changes.map(function (o) {
            if (o.isNew)
                return "Added new property \"".concat(o.property, "\"");
            else
                return "Changed value of \"".concat(o.property, "\"");
        });
        return log.join("\n");
    };
    // TODO: Should this be private
    /***
     *
     * @ignore
     * @param property
     * @param newValue
     * @param oldValue
     * @private
     */
    Store.prototype._triggerChangeCallbacks = function (property, newValue, oldValue) {
        if (!this.onChangeCallbacks[property])
            return;
        this.onChangeCallbacks[property].forEach(function (callback) { return callback(newValue, oldValue, property); });
    };
    /**
     * Returns change log of entire history in human-readable form
     * @return {string}
     */
    Store.prototype.getChangeLog = function () {
        var fullHistory = this.getFullHistory();
        var changes = [];
        fullHistory.forEach(function (o, i) {
            if (i == 0)
                return;
            changes.push(Store.getChangeLog(fullHistory[i - 1], o));
        });
        return changes.join("\n------------\n");
    };
    return Store;
}());
exports.Store = Store;
exports.default = Store;
//# sourceMappingURL=Store.js.map