Home Reference Source Repository

src/ObjUtil.js

let Immutable;

// taken from NISV client project
// 
/**
 * A class with static util methods for working with objects.
 *
 * @author jovica.aleksic <jovica.aleksic@xailabs.de>
 */
export default class ObjUtil {

    /**
     * Register the `immutable.js` library.
     * This is only required by the `pick()` method when used on immutable objects.
     *
     * @param {object} immutable - The `immutable.js` library export.
     * @example ObjUtil.registerImmutable(require('immutable'))
     */
    static registerImmutable(immutable) {
        Immutable = immutable;
    }

    /**
     * Performs a shallow comparison of two objects and returns an array containing the names of changed properties, or `null` if all values were strictly equal.
     * Works with both immutable and plain JS objects.
     *
     * Can be used for shouldComponentUpdate, e.g. `if (Obj.diff(this.props, nextProps, ['name', 'address'])) { ... }`
     * is basically the same as `if (this.props.name !== nextProps.name || this.props.address !== nextProps.address) { ... }`
     *
     * @param {object} a - The first object
     * @param {object} b - The second object
     * @param {Array<String>} names - An array of property names to compare or null if nothing was changed
    */
    static diff(a, b, names) {
        if (names && !Array.isArray(names)) {
            names = Object.keys(names);
        }
        const diff = [];
        if (!a && b) return true;
        if (a && !b) return true;
        if (!a && !b) return null;
        const getValue = (obj, prop) => {
            if (obj === null || obj === undefined) {
                return null;
            } else {
                return typeof obj.get === 'function' ? obj.get(prop) : obj[prop];
            }
        };
        if (!names) {
            let aKeys;
            if (a) {
                aKeys = Object.keys(a.toJS ? a.toJS() : a);
            }
            else {
                aKeys = [];
            }
     
            let bKeys;
            if (b) {
                bKeys = Object.keys(b.toJS ? b.toJS() : b);
            }
            else {
                bKeys = [];
            }
            names = aKeys.concat(bKeys.filter(key => aKeys.indexOf(key) === -1));
        }
        for (let name of names) {
            // console.warn('>>', {name, a, b}, {aValue: getValue(a, name), bValue: getValue(b, name)}, getValue(a, name) !== getValue(b, name))
            if (getValue(a, name) !== getValue(b, name) && diff.indexOf(name) === -1) {
                diff.push(name);
            }
        }
        if (diff.length === 0) {
            return null;
        }
        return diff;
    }

    /**
     * Returns an array containing all values of an object, like as `Object.values` in ES6.
     *
     * @param {object} obj - The object to inspect
     * @return {Array<*>} - The values of all object keys. Order is not safe.
     */
    static values(obj) {
        return Object.keys(obj).map(key => obj[key]);
    }

    /**
     * Like `Object.keys`, but allows you to omit certain names.
     *
     * @param {object} obj - An object
     * @param {object} [options] - An object with optional configuration
     * @param {Array} [options.not] - An array of property names to exclude
     * @return {object} A new object containing all keys (property names) of the object except the excluded ones
     * @example Obj.keys(this.props, {not: ['onClick']})
     */
    static keys(obj, options) {
        let keys = Object.keys(obj);
        if (options && options.not) {
            keys = keys.filter(key => options.not.indexOf(key) === -1);
        }
        return keys;
    }

    /**
     * Returns an object containing all properties except the ones specified via `{not: [...]}`
     *
     * @param {object} obj - An object
     * @param {object} [options] - An object with optional configuration
     * @param {Array} [options.not] - An array of property names to exclude
     * @return {object} A new object containing all properties except the excluded ones
     *
     * @example
     * const clean = Obj.rest({a: 'a', b: 'b', c: 'c'}, {not: ['b', 'c']}); // {a: 'a'}
     */
    static rest(obj, options) {
        const keys = this.keys(obj, options);
        return keys.reduce((result, key) => {
            result[key] = obj[key];
            return result;
        }, {});
    }

    /**
     * Returns a new object that contains only specific values of an original object.
     * If the original object was immutable, the result will be immutable too.
     *
     * @param {object} obj - An object
     * @param {Array} keys - An array of property names to pick from the object
     * @return {object} A new object containing the specified properties
     */
    static pick(obj, keys) {
        if (!obj) {
            return null;
        }
        if (!keys || keys.length === 0) {
            // no keys specified. return new object containing everything
            return obj.toJS ? obj.merge({}) : { ...obj };
        }

        if (obj && obj.toJS) {
            // obj is immutable, keys are given
            // see https://github.com/facebook/immutable-js/wiki/Predicates#pick--omit
            if (!Immutable) {
                throw new Error(
                    'You must register Immutable.js in order to use pick() with immutable objects and specific keys. Use registerImmutable(require("immutable"))'
                );
            }
            function keyIn(...keys) {
                var keySet = Immutable.Set(keys);
                return function(v, k) {
                    return keySet.has(k);
                };
            }
            return obj.filter(keyIn(...keys));
        }

        // obj is plain object
        return keys.reduce((result, key) => {
            result[key] = obj[key];
            return result;
        }, {});
    }

    /**
     * Deeply converts an immutable or mixed value to a plain javascript object.
     * If the given object itself was a primitive, `undefined` or `null`, it is returned as-is.
     *
     * @param {any} value - A plain or immutable value
     * @return {any} - A plain object or a primitive value.
     */
    static pojo(value) {
        if (value === undefined || value === null) {
            return value;
        }
        if (value !== Object(value)) {
            // primitive, e.g. string or number
            return value;
        }
        if (typeof value.toJS === 'function') {
            return value.toJS();
        }
        if (Array.isArray(value)) {
            return value.map(v => this.pojo(v));
        }
        return Object.keys(value).reduce((result, key) => {
            result[key] = this.pojo(value[key]);
            return result;
        }, {});
    }
}