validated-changeset
Version:
Changesets for your local state
102 lines • 4.33 kB
JavaScript
import Change, { getChangeValue, isChange } from '../-private/change';
import isObject from './is-object';
import { isArrayObject } from './array-object';
function split(path) {
const keys = path.split('.');
return keys;
}
function findSiblings(target, keys) {
const [leafKey] = keys.slice(-1);
const remaining = Object.keys(target)
.filter((k) => k !== leafKey)
.reduce((acc, key) => {
acc[key] = target[key];
return acc;
}, Object.create(null));
return Object.assign({}, remaining);
}
function isValidKey(key) {
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
}
/**
* TODO: consider
* https://github.com/emberjs/ember.js/blob/822452c4432620fc67a777aba3b150098fd6812d/packages/%40ember/-internals/metal/lib/property_set.ts
*
* Handles both single path or nested string paths ('person.name')
*
* @method setDeep
*/
export default function setDeep(target, path, value, options = { safeSet: undefined, safeGet: undefined }) {
const keys = split(path).filter(isValidKey);
// We will mutate target and through complex reference, we will mutate the orig
let orig = target;
options.safeSet =
options.safeSet ||
function (obj, key, value) {
return (obj[key] = value);
};
options.safeGet =
options.safeGet ||
function (obj, key) {
return obj ? obj[key] : obj;
};
if (keys.length === 1) {
options.safeSet(target, path, value);
return target;
}
for (let i = 0; i < keys.length; i++) {
let prop = keys[i];
if (Array.isArray(target) && parseInt(prop, 10) < 0) {
throw new Error('Negative indices are not allowed as arrays do not serialize values at negative indices');
}
const isObj = isObject(options.safeGet(target, prop));
const isArray = Array.isArray(options.safeGet(target, prop));
const isComplex = isObj || isArray;
if (!isComplex) {
options.safeSet(target, prop, {});
}
else if (isComplex && isChange(options.safeGet(target, prop))) {
let changeValue = getChangeValue(options.safeGet(target, prop));
if (isObject(changeValue)) {
// if an object, we don't want to lose sibling keys
const siblings = findSiblings(changeValue, keys);
const resolvedValue = isChange(value) ? getChangeValue(value) : value;
const isArrayLike = Array.isArray(target) || isArrayObject(target);
const nestedKeys = isArrayLike
? keys.slice(i + 1, keys.length).join('.') // remove first key segment as well as the index
: keys.slice(1, keys.length).join('.'); // remove first key segment
let newValue;
// if the resolved value was deleted (via setting to null or undefined),
// there is no need to setDeep. We can short-circuit that and set
// newValue directly because of the shallow value
if (isArrayLike && undefined === resolvedValue) {
newValue = resolvedValue;
}
else if (i === keys.length - 1) {
// If last key, this is the final value
newValue = resolvedValue;
}
else {
newValue = setDeep(siblings, nestedKeys, resolvedValue, options);
}
options.safeSet(target, prop, new Change(newValue));
// since we are done with the `path`, we can terminate the for loop and return control
break;
}
else {
// we don't want to merge new changes with a Change instance higher up in the obj tree
// thus we nullify the current Change instance to
options.safeSet(target, prop, {});
}
}
// last iteration, set and return control
if (i === keys.length - 1) {
options.safeSet(target, prop, value);
break;
}
// assign next level of object for next loop
target = options.safeGet(target, prop);
}
return orig;
}
//# sourceMappingURL=set-deep.js.map