3

I have an array of objects that can be of any length and any depth. I need to be able to find an object by its id and then modify that object within the array. Is there an efficient way to do this with either lodash or pure js?

I thought I could create an array of indexes that led to the object but constructing the expression to access the object with these indexes seems overly complex / unnecessary

edit1; thanks for all yours replies I will try and be more specific. i am currently finding the location of the object I am trying to modify like so. parents is an array of ids for each parent the target object has. ancestors might be a better name for this array. costCenters is the array of objects that contains the object I want to modify. this function recurses and returns an array of indexes that lead to the object I want to modify

var findAncestorsIdxs = function(parents, costCenters, startingIdx, parentsIdxs) {
            var idx = startingIdx ? startingIdx : 0;
            var pidx = parentsIdxs ? parentsIdxs : [];

            _.each(costCenters, function(cc, ccIdx) {
                if(cc.id === parents[idx]) {
                    console.log(pidx);
                    idx = idx + 1;
                    pidx.push(ccIdx);
                    console.log(pidx);
                    pidx = findAncestorsIdx(parents, costCenters[ccIdx].children, idx, pidx);
                }
            });
            return pidx;
        }; 

Now with this array of indexes how do I target and modify the exact object I want? I have tried this where ancestors is the array of indexes, costCenters is the array with the object to be modified and parent is the new value to be assigned to the target object

var setParentThroughAncestors = function(ancestors, costCenters, parent) {
            var ccs = costCenters;
            var depth = ancestors.length;
            var ancestor = costCenters[ancestors[0]];
            for(i = 1; i < depth; i++) {
                ancestor = ancestor.children[ancestors[i]];
            }
            ancestor = parent;
            console.log(ccs);
            return ccs;
        };

this is obviously just returning the unmodified costCenters array so the only other way I can see to target that object is to construct the expression like myObjects[idx1].children[2].grandchildren[3].ggranchildren[4].something = newValue. is that the only way? if so what is the best way to do that?

Mark
  • 3,137
  • 4
  • 39
  • 76
  • You have to iterate recursively over all objects and compare against the ID you are looking for. – Felix Kling Aug 29 '14 at 13:28
  • You say you have an array of objects. Arrays in JavaScript are indexed, not associative, so presumably you mean find an object by its index, not ID. Else, please clarify. – Mitya Aug 29 '14 at 13:28
  • each object in the array has a unique id. I want to find a given object by that id and then change that object – Mark Aug 29 '14 at 13:33
  • @FelixKling and when I get to the id I am looking for how to I edit that object directly within the array? – Mark Aug 29 '14 at 13:34
  • You can just assign directly to the object. E.g `if (obj.id === idToFind) { obj.prop = newValue; }` – Felix Kling Aug 29 '14 at 13:38
  • Do you mean id as in the value of a key named `id`, or key? –  Aug 29 '14 at 13:44
  • value of a key named id – Mark Aug 29 '14 at 13:45
  • 1
    See [Access / process (nested) objects, arrays or JSON](http://stackoverflow.com/q/11922383/218196). It has a section about recursion. – Felix Kling Aug 29 '14 at 13:49
  • 1
    You're now stating the problem in a way which presupposes a particular, awkward-to-impossible solution, which is to return some kind of "index" into an object which can then be used to reference something down within the object. But there is no such animal. You are better off designing your solution so that you pass a callback which is invoked to handle the modification when the sub-object of interest is found. –  Aug 29 '14 at 14:19
  • That makes sense i wasn't trying to imply that this is the way I have to do it figure it out. Just that this is the way I thought I could do it is it right / wrong whats the alternative. If I am understanding everything correctly i think @NicolaeS recursion with your suggestion of a callback to modify the value will work great. – Mark Aug 29 '14 at 14:26

6 Answers6

18

You can use JSON.stringify for this. It provides a callback for each visited key/value pair (at any depth), with the ability to skip or replace.

The function below returns a function which searches for objects with the specified ID and invokes the specified transform callback on them:

function scan(id, transform) {
  return function(obj) {
    return JSON.parse(JSON.stringify(obj, function(key, value) {
      if (typeof value === 'object' && value !== null && value.id === id) {
        return transform(value);
      } else {
        return value;
      }
  }));
}

If as the problem is stated, you have an array of objects, and a parallel array of ids in each object whose containing objects are to be modified, and an array of transformation functions, then it's just a matter of wrapping the above as

for (i = 0; i < objects.length; i++) {
    scan(ids[i], transforms[i])(objects[i]);
}

Due to restrictions on JSON.stringify, this approach will fail if there are circular references in the object, and omit functions, regexps, and symbol-keyed properties if you care.

See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter for more info.

  • 4
    Nice! didn't know `JSON.stringify` could accept callbacks. – Hrishi Aug 31 '14 at 04:18
  • 1
    This is very useful information. I wish I found it before I spent an embarrassingly long time trying to traverse an object tree with a redux-persist transformer just to hunt for date strings to turn into Date objects! – Eric G Jul 17 '16 at 04:17
  • Amazing! +1 for explaining the limitations of this approach as well! – Master of Ducks Oct 05 '19 at 20:08
2

As Felix Kling said, you can iterate recursively over all objects.

// Overly-complex array
var myArray = {
    keyOne: {},
    keyTwo: {
        myId: {a: '3'}
    }
};
var searchId = 'myId', // Your search key
    foundValue, // Populated with the searched object
    found = false; // Internal flag for iterate()

// Recursive function searching through array
function iterate(haystack) {
    if (typeof haystack !== 'object' || haystack === null) return; // type-safety
    if (typeof haystack[searchId] !== 'undefined') {
        found = true;
        foundValue = haystack[searchId];
        return;
    } else {
        for (var i in haystack) {
            // avoid circular reference infinite loop & skip inherited properties
            if (haystack===haystack[i] || !haystack.hasOwnProperty(i)) continue;

            iterate(haystack[i]);
            if (found === true) return;
        }
    }
}

// USAGE / RESULT
iterate(myArray);
console.log(foundValue); // {a: '3'}
foundValue.b = 4; // Updating foundValue also updates myArray
console.log(myArray.keyTwo.myId); // {a: '3', b: 4}

All JS object assignations are passed as reference in JS. See this for a complete tutorial on objects :)

Edit: Thanks @torazaburo for suggestions for a better code.

NicolaeS
  • 423
  • 2
  • 7
  • thanks for your reply but this dosnt tell me how to modify the found object – Mark Aug 29 '14 at 14:15
  • 1
    You modify foundValue directly, as it's just a reference to myArray. See my updated "RESULT" section above. – NicolaeS Aug 29 '14 at 14:18
  • 1
    ok im with you thanks I will give this a try and see how I go – Mark Aug 29 '14 at 14:23
  • Need to add `typeof haystack !== null`. Also, this code will halt when the first matching id is found. –  Aug 29 '14 at 14:28
  • @torazaburo can you be more specific please? I included a strict type safety with `typeof haystack !== 'object'`. Also, the `if (found === true) return;` should stop all recursive loops right after the value is found. – NicolaeS Aug 29 '14 at 14:34
  • @torazaburo: `typeof` always returns a string. It does never return `null` or `"null"`. – Felix Kling Aug 29 '14 at 15:10
  • 1
    @NicolaeS: `typeof null` returns `"object"` as well. You'd want something like `if (!haystack || typeof haystack !== 'object')` – Felix Kling Aug 29 '14 at 15:11
  • 1
    Sorry, I meant `haystack !== null`. –  Aug 29 '14 at 16:15
  • A little bit confused as to why this would be the accepted answer. It does not match the spec that the object to be found should be the one with an `id` property of a specified value; instead, it returns an object with a key equal to some value. It does not deal with circular structures. It fails to do the requisite `isOwnProperty` check within the `for in` loop. As already mentioned, it fails to deal with `null` values. –  Aug 29 '14 at 16:45
  • @torazaburo and @FelixKling Thanks for your constructive feedback! I updated the code accordingly, hope it's OK now. As for the spec issue: the function returns the first object associated with the `searchId` key, at least that's how I understood Ir1sh's question. – NicolaeS Aug 30 '14 at 19:12
1

If each object has property with the same name that stores other nested objects, you can use: https://github.com/dominik791/obj-traverse

findAndModifyFirst() method should solve your problem. The first parameter is a root object, not array, so you should create it at first:

var rootObj = {
  name: 'rootObject',
  children: [
    {
      'name': 'child1',
       children: [ ... ]
    },
    {
       'name': 'child2',
       children: [ ... ]
    }
  ]
};

Then use findAndModifyFirst() method:

findAndModifyFirst(rootObj, 'children', { id: 1 }, replacementObject)

replacementObject is whatever object that should replace the object that has id equal to 1.

You can try it using demo app: https://dominik791.github.io/obj-traverse-demo/

dominik791
  • 692
  • 6
  • 17
0

Here's an example that extensively uses lodash. It enables you to transform a deeply nested value based on its key or its value.

const _ = require("lodash")
const flattenKeys = (obj, path = []) => (!_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}));


const registrations = [{
  key: "123",
  responses:
  {
    category: 'first',
  },
}]


function jsonTransform (json, conditionFn, modifyFn) {

  // transform { responses: { category: 'first' } } to { 'responses.category': 'first' }
  const flattenedKeys = Object.keys(flattenKeys(json));

  // Easily iterate over the flat json
  for(let i = 0; i < flattenedKeys.length; i++) {
    const key = flattenedKeys[i];
    const value = _.get(json, key)

    // Did the condition match the one we passed?
    if(conditionFn(key, value)) {

      // Replace the value to the new one    
      _.set(json, key, modifyFn(key, value)) 
    }
  }

  return json
}

// Let's transform all 'first' values to 'FIRST'
const modifiedCategory = jsonTransform(registrations, (key, value) => value === "first", (key, value) => value = value.toUpperCase())

console.log('modifiedCategory --', modifiedCategory)
// Outputs: modifiedCategory -- [ { key: '123', responses: { category: 'FIRST' } } ]
aquiseb
  • 949
  • 8
  • 17
0

I needed to modify deeply nested objects too, and found no acceptable tool for that purpose. Then I've made this and pushed it to npm.

https://www.npmjs.com/package/find-and

This small [TypeScript-friendly] lib can help with modifying nested objects in a lodash manner. E.g.,

var findAnd = require("find-and");

const data = {
  name: 'One',
  description: 'Description',
  children: [
    {
      id: 1,
      name: 'Two',
    },
    {
      id: 2,
      name: 'Three',
    },
  ],
};

findAnd.changeProps(data, { id: 2 }, { name: 'Foo' });

outputs

{
  name: 'One',
  description: 'Description',
  children: [
    {
      id: 1,
      name: 'Two',
    },
    {
      id: 2,
      name: 'Foo',
    },
  ],
}

https://runkit.com/embed/bn2hpyfex60e

Hope this could help someone else.

Arfeo
  • 870
  • 7
  • 20
0

I wrote this code recently to do exactly this, as my backend is rails and wants keys like:

first_name

and my front end is react, so keys are like:

firstName

And these keys are almost always deeply nested:

user: {
  firstName: "Bob",
  lastName: "Smith",
  email: "bob@email.com"
}

Becomes:

user: {
  first_name: "Bob",
  last_name: "Smith",
  email: "bob@email.com"
}

Here is the code

function snakeCase(camelCase) {
  return camelCase.replace(/([A-Z])/g, "_$1").toLowerCase()
}

export function snakeCasedObj(obj) {
  return Object.keys(obj).reduce(
    (acc, key) => ({
      ...acc,
      [snakeCase(key)]: typeof obj[key] === "object" ? snakeCasedObj(obj[key]) : obj[key],
    }), {},
  );
}

Feel free to change the transform to whatever makes sense for you!