I have an object with nested properties. I want to update those nested properties by using an array of elements that reflect the nested sequence of the object. Also if a property does not exist, then add it, otherwise only update it.
Problem: is that I got all properties separated (non nested) with just empty objects ...
This is the function I tried implementing
function myFunction(myObject, ...myArrray) {
let value = myArrray.pop()
for (let i = 0; i < myArrray.length; i++) {
const element = myArrray[i]
if (!myObject.hasOwnProperty(element)) {
if (i == myArrray.length)
myObject[element] = value
else
myObject[element] = {}
}
else {
myObject[element] = myObject[i + 1]
}
}
return myObject
}
const myLocalStorage = {
users: {
theme: 'westeros',
notifications: {
email: true,
push: {
infos: true
}
}
},
admins: {
// admin properties
}
}
Updating "email" property (which exists)
const array1 = ["users", "notification", "email", false]
myFunction(myLocalStorage, array1)
- Adding "sms" property (which does not exist)
const array2 = ["users", "notification", "sms", true]
myFunction(myLocalStorage, array2)
5 Answers 5
There are multiple issues with your code; most importantly you need to update the object you'll assign to, following the path you have followed to the current point.
In the following implementation, I defined the targetObject variable that follows the path from myObject up to the penultimate key (targetObject = targetObject[key]); all those values (targetObject[key]) need to be Objects. Then the targetObject will be assigned the value at the final key:
function myFunction(myObject, myArray) {
const value = myArray.pop();
let targetObject = myObject;
for (let i = 0; i < myArray.length - 1; i++) {
const key = myArray[i];
if(!(targetObject[key] instanceof Object)){
targetObject[key] = {};
}
targetObject = targetObject[key];
}
const key = myArray[myArray.length - 1];
targetObject[key] = value;
return myObject; // not necessary if we have the original array
}
Demo snippet:
function myFunction(myObject, myArray) {
const value = myArray.pop();
let targetObject = myObject;
for (let i = 0; i < myArray.length - 1; i++) {
const key = myArray[i]
if(!(targetObject[key] instanceof Object)){
targetObject[key] = {};
}
targetObject = targetObject[key];
}
const key = myArray[myArray.length - 1];
targetObject[key] = value;
return myObject;
}
const myLocalStorage = {
users: {
theme: 'westeros',
notifications: {
email: true,
push: {
infos: true
}
}
},
admins: {
// admin properties
}
}
const array1 = ["users", "notifications", "email", false];
myFunction(myLocalStorage, array1);
const array2 = ["users", "notifications", "sms", true]
myFunction(myLocalStorage, array2)
console.log(myLocalStorage);
This can be written somewhat more concisely using Array#reduce:
function myFunction(myObject, myArray) {
const value = myArray.pop();
const lastKey = myArray.pop();
const innerObject = myArray.reduce((targetObject, key) => {
if(!(targetObject[key] instanceof Object)){
targetObject[key] = {};
}
return targetObject[key];
}, myObject );
innerObject[lastKey] = value;
}
The same effect can be obtained through recursion, by calling recursively myFunction with an object ever more deep inside the structure of the original object and with an array ever smaller:
function myFunction(myObject, myArray){
if(myArray.length === 0){
return;
}
const key = myArray.shift();
if(myArray.length === 1){
myObject[key] = myArray[0];
}
else{
if(!(myObject[key] instanceof Object)){
myObject[key] = {}
}
myFunction(myObject[key], myArray);
}
}
Demo snippet:
function myFunction(myObject, myArray){
if(myArray.length === 0){
return;
}
const key = myArray.shift();
if(myArray.length === 1){
myObject[key] = myArray[0];
}
else{
if(!(myObject[key] instanceof Object)){
myObject[key] = {}
}
myFunction(myObject[key], myArray);
}
}
const myLocalStorage = {
users: {
theme: 'westeros',
notifications: {
email: true,
push: {
infos: true
}
}
},
admins: {
// admin properties
}
}
const array1 = ["users", "notifications", "email", false]
myFunction(myLocalStorage, array1);
const array2 = ["users", "notifications", "sms", true]
myFunction(myLocalStorage, array2)
console.log(myLocalStorage);
Comments
You could pop the last key as well and iterate the path to final object.
This approach does not use the rest syntax, because the update data with keys and value is already an array.
function update(object, data) {
const
value = data.pop(),
lastKey = data.pop();
let temp = object;
for (const key of data) temp = temp[key] ??= {};
temp[lastKey] = value;
return object;
}
const
myLocalStorage = { users: { theme: 'westeros', notifications: { email: true, push: { infos: true } } }, admins: { content: 'admin properties' } },
array1 = ["users", "notifications", "email", false];
console.log(update(myLocalStorage, array1));
1 Comment
{email: true} to {email: {sent: true}}, see jsFiddle. The OP didn't mention this, so, of course, either behaviour might be considered correct.I afforded myself some fun creating an alternative for @kikon's fine answer.
The functionallity to update/add (nested) properties to an Object is pulled apart in a few functions.
The path passed to the updateProp function is now a string (properties divided with '.' or '/'). A non existing property will be added, an existing property will be modified. updateProp always returns the complete (modified) Object.
Maybe useful. Here's a more comprehensive codepen to fiddle with. Also as a small stackblitz project.
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;
}
1 Comment
You can iteratively get into the sub-objects of the object by the key array, create what's missing and update what's existing:
function myFunction(obj, array) {
for (let index = 0; index < array.length - 2; index++) {
if (!obj[array[index]]) {
obj[array[index]] = {};
}
obj = obj[array[index]];
}
obj[array[array.length - 2]] = array[array.length - 1];
}
const myLocalStorage = {
users: {
theme: 'westeros',
notifications: {
email: true,
push: {
infos: true
}
}
},
admins: {
// admin properties
}
};
const array1 = ["users", "notifications", "email", false];
myFunction(myLocalStorage, array1);
const array2 = ["users", "notifications", "sms", true];
myFunction(myLocalStorage, array2);
console.log(myLocalStorage);
Comments
Here's a set function that doesn't mutate both arguments but I've got to admit that the "cloning" bit is naive at best (most likely pure junk). Anyway there you go:
function set(o, path) {
const clone = JSON.parse(JSON.stringify(o));
function recur(acc, [key, ...rest]) {
if (rest.length <= 1) {
acc[key] = rest[0];
return clone;
}
acc[key] ??= {};
return recur(acc[key], rest);
}
return recur(clone, path);
}
p = {};
n = set(p, ["users", "notifications", "email", false]);
//=> {users: {notifications: {email: false}}}
n = set(n, ["users", "notifications", "email", true]);
//=> {users: {notifications: {email: true}}}
n = set(n, ["users", "notifications", "sms", true]);
//=> {users: {notifications: {email: true, sms: true}}}
p;
//=> {}
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)