7

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
  • 2
    If you want to simplify your life a bit, lodash set function does almost that (lodash.com/docs#set). _.set(myLocalStorage, "users.notification.email", false); Commented Nov 2 at 12:24
  • Thanks @njzk2. I just did'nt want to load a whole module for something I might be able to do with ten lines of code. Thanks for the tip anyway. Commented Nov 2 at 13:12
  • See How to set object property (of object property of …) given its string name in JavaScript? or Dynamic deep setting for a JavaScript object for the general solution. The answers here should just be concerned with what is wrong with the specific code in the question. Commented Nov 3 at 13:02
  • 1
    @a6eb0a I would recommend to pass the value to be set as a separate parameter, not as the last element of the array with the property path. Commented Nov 3 at 13:02
  • (you can either use the single lodash/set module (npmjs.com/package/lodash.set), or rely on your bundler to tree-shake it and include only what's needed) Commented Nov 4 at 19:16

5 Answers 5

9

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);

Sign up to request clarification or add additional context in comments.

Comments

6

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

Nice short one! It's worth mentioning that testing for nullish vs testing for Object makes a difference in semantics for an unusual corner case: when an existing leaf value (say a boolean) is to be replaced by a recursive structure (an object) by "lengthening" the path. e.g., {email: true} to {email: {sent: true}}, see jsFiddle. The OP didn't mention this, so, of course, either behaviour might be considered correct.
4

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

Very Interesting @KooiInc. This approach could be helpul in some context. Thanks for sharing !!!!
2

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

0

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;
//=> {}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.