2

I have an array of objects. Each object can also contain an array of objects, and so on to an arbitrary depth.

var myArray = [
    { 
        id:'foo', 
        items:[]
    },
    {
        id:'bar', 
        items:[
            {
                id:'blah'
                items:[...etc...]
            }
        ]
    }
]

I'd like to read, add, and remove objects in the nested arrays using an array of indices.

So a function to remove the value myArray[1][3][2] from myArray would be called with this array of indexes as a parameter: [1, 3, 2]

I've found that you can use reduce() to return a value like so:

indices.reduce((acc, cur) => Array.isArray(acc) ? acc[cur] : acc.items[cur], myArray)

but cannot work out how to remove or add a value using the same idea. Any help is much appreciated.

3 Answers 3

3

You could create a function which takes similar arguments as the splice function. Pass the nested array, the indices path, the total number of items to be deleted and collect all the new items to be added at the end using rest parameters.

function deepSplice(array, indices, deleteCount, ...toBeInserted) {
  const last = indices.pop();
  const finalItems = indices.reduce((acc, i) => acc[i].items, array);
  finalItems.splice(last, deleteCount, ...toBeInserted);
  return array
}
  • Remove the last index from the indices array.
  • reduce the indices array to get the nested items array in every loop to get the final items array you want to do the insert/delete operation on.
  • Use splice on the last index to insert/delete based on the argument passed.

If you just want to insert, pass deleteCount = 0. And if you just want to remove, skip the last argument.

Here's a snippet:

const myArray = [
  { id: "0", items: [] },
  {
    id: "1",
    items: [
      {
        id: "1.0",
        items: [
          { id: "1.0.0", items: [] }, 
          { id: "1.0.1", items: [] }]
      },
      { id: "1.1", items: [] }
    ]
  }
];

function deepSplice(array, indices, deleteCount, ...toBeInserted) {
  const last = indices.pop();
  const finalItems = indices.reduce((acc, i) => acc[i].items, array);
  finalItems.splice(last, deleteCount, ...toBeInserted);
  return array
}

console.log(
  // removes "1.0.1" item and inserts a new object there
  deepSplice(myArray, [1,0,1], 1, { id: 'newlyInserted'})
)

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

5 Comments

Thanks very much. I've tested it and it works perfectly. Edit: I've updated the question with the correct plural of index.
In deepSplice, you modify finalItems but then return the original array. How does that work?
@SeanMC If you modify the nested object / array, the original object will also get modified because it holds a reference
Thanks I figured I just didn’t see where the reference was getting passed. Think I see it now thanks
@SeanMC every nested object is a different object. So, const o = { a : { b: 10 } }; const nested = o.a and if you set nested.b = 15 And console.log(o) it will have the updated nested object because o.a is a different object in itself. In other words, the a property of the o object is assigned with an object.
2

The easiest way is to use only the indices without the last one to have it used for any operation as you want.

For deleting, you need this index to splice the array, and as well for updating.

In this case, you could return the parent object and use an object with items as property for the given array as start value for reducing.

This allows to access the parent object and use items for any further operation.

lastIndex = indices.pop();
parent = indices.reduce((r, index) => r.items[index], { items: myArray });

// further use
parent.items.splice(lastIndex, 1); // delete

1 Comment

Thank you, this answer is correct, but I have awarded the correct answer to @adiga as their answer was more detailed.
0

Here is a solution using object-scan.

The main advantage of using object-scan is that you get more control to adjust your functions (e.g. if you wanted to get multiple entries or do fuzzy key matching etc). However there is increased complexity, so it's a trade-off and depends on your requirements.

// const objectScan = require('object-scan');

const myArray = [{ id: '0', items: [] }, { id: '1', items: [ { id: '1.0', items: [{ id: '1.0.0' }, { id: '1.0.1' }] }, { id: '1.1', items: [] } ] }];

const indicesToNeedle = (indices) => indices.map((idx) => `[${idx}]`).join('.items');

const get = (array, indices) => objectScan(
  [indicesToNeedle(indices)],
  { abort: true, rtn: 'value' }
)(array);

const splice = (array, indices, deleteCount, ...toBeInserted) => objectScan(
  [indicesToNeedle(indices)],
  {
    abort: true,
    rtn: 'bool', // returns true iff spliced
    filterFn: ({ parent, property }) => {
      parent.splice(property, deleteCount, ...toBeInserted);
      return true;
    }
  }
)(array);

console.log(get(myArray, [1, 0, 1]));
// => { id: '1.0.1' }
console.log(get(myArray, [1, 0, 2]));
// => undefined

// removes "1.0.1" item and inserts two objects there
console.log(splice(myArray, [1, 0, 1], 1, { id: '1.0.1-new' }, { id: '1.0.2-new' }));
// => true
console.log(myArray);
// => [ { id: '0', items: [] }, { id: '1', items: [ { id: '1.0', items: [ { id: '1.0.0' }, { id: '1.0.1-new' }, { id: '1.0.2-new' } ] }, { id: '1.1', items: [] } ] } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

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.