const myLocalStorage = {
users: {
theme: 'westeros',
notifications: {
email: true,
push: {
infos: true
}
}
},
admins: {}
};
// update or add a (nested) property to [obj]
function updateProp(obj, { path, newValue } = {}) {
const { found, lastKey } = maybePath(obj, path);
if (found) { found[lastKey] = newValue; }
return obj;
}
// Examples
log(
`updateProp(myLocalStorage, {path: "HELLO", newValue: "WORLD"}).HELLO`,
updateProp(myLocalStorage, {path: "HELLO", newValue: "WORLD"}).HELLO);
// Note: escaped key [...] (see resolvePath function)
log(
`// an escaped key in the path [...]<br>` +
`updateProp(myLocalStorage, {path: "[HELLO / WORLD]", newValue: "WORLD"})`+
`["HELLO / WORLD"]`,
updateProp(myLocalStorage, {path: "[HELLO / WORLD]", newValue: "WORLD"})
["HELLO / WORLD"]);
log(
`updateProp(myLocalStorage, {path: "users/notifications/email",`+
` newValue: [{address: "[email protected]", send: true}] })`+
`.users.notification.email`,
updateProp(myLocalStorage, {
path: "users/notifications/email",
newValue: [{address: "[email protected]", send: true}]})
.users.notifications.email);
log(
`updateProp(myLocalStorage, {path: "admins", newValue: "Mary"}).admins`,
updateProp(myLocalStorage, { path: "admins", newValue: "Mary" }).admins);
log(
`updateProp(myLocalStorage, {path: "admins", `+
`newValue: { main: "Mary Bushel", local: "Mary Bushel's sister" } })` +
`.admins`,
updateProp(myLocalStorage, { path: "admins",
newValue: { main: "Mary Bushel", local: "Mary Bushel's sister" } })
.admins);
log(
`updateProp(myLocalStorage) `+
`// does nothing, returns [myLocalStorage]`,
updateProp(myLocalStorage));
log(
`updateProp(myLocalStorage, {newValue: ""}) `+
`// does nothing, returns [myLocalStorage]`,
updateProp(myLocalStorage, {newValue: ""}));
// is maybeObject really an Object?
function isObject(maybeObj) {
return !Array.isArray(maybeObj) &&
maybeObj?.constructor === Object;
}
// Extract an array from a (possible) path string
// Note: a(n Object) key can be any string.
// When a key contains dots or forward slashes
// escape it using square brackets.
// e.g. [my.key.here] or [my / key / here]
function resolvePath(path) {
path = path?.split(``) || [``];
const keys = [];
let key = ``;
let escaped = false;
for (let chr of path) {
switch (true) {
case chr === `[`:
key = ""; escaped = true; break;
case chr === `]` && escaped:
escaped = false; break;
case !/[\/.\]]/.test(chr) || escaped:
key += chr; break;
default:
keys.push(key); key = ""; escaped = false;
}
}
keys.push(key);
return keys.map(v => v.trim()).filter(v => v.length > 0);
}
// retrieve a path recursively from [obj]
// with a given path array
function retrievePath(obj, path) {
const key = path.shift();
obj = key in obj && isObject(obj[key])
? obj[key] : obj;
return path.length > 0
? retrievePath(obj, path)
: { found: obj, lastKey: key };
}
// try retrieving a path from [obj] with a
// possible path array
function maybePath(obj, path) {
path = resolvePath(path);
let lastKey = path?.at(-1) ?? null;
switch (true) {
case path.length < 1: return { found: null, lastKey };
case path.length === 1:
lastKey = path[0];
return {
found: obj,
lastKey };
default: return retrievePath(obj, path);
}
}
// demo: log to screen
function log(cmd, obj) {
document.body.insertAdjacentHTML(`beforeend`,
`<code>${cmd}</code>
<pre>${JSON.stringify(obj, null, 2)}</pre>`);
}
code {
background-color: rgb(227, 230, 232);
color: rgb(12, 13, 14);
padding: 0 4px;
display: inline-block;
border-radius: 4px;
font-family: monospace;
font-size: 85%;
position: relative;
}
pre {
margin-top: 0.2em;
}
setfunction does almost that (lodash.com/docs#set)._.set(myLocalStorage, "users.notification.email", false);lodash/setmodule (npmjs.com/package/lodash.set), or rely on your bundler to tree-shake it and include only what's needed)