Using an array of indices to access and modify arbitrarily deep nested arrays in Javascript

Viewed 532

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

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'})
)

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

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/object-scan@13.8.0"></script>

Disclaimer: I'm the author of object-scan

Related