0

Link to sandbox: https://codesandbox.io/s/cool-northcutt-7c9sr

I have a catalog with furniture and I need to make dynamic breadcrumbs. There is an array with nested objects which 5 levels deep. When I'm rendering list of furniture, I 'm saving all indexes from which array this list.

Expected output: Using my indexes, I need to parse object with nested array , get name of each object where belongs that index and save it in array

Indexes that I saved when user clicked on inventories . Key is a object name and property is actual index.

menuIndexes : {
  buildings: 0,
  building_styles: 3,
  rooms: 2,
  room_fillings: 0,
  filling_types: 1,
}

That piece of data , where i'm rendering from list of furniture . Name property is a name of a link in menu

{
  buildings: [
    {
      name: 'office',
      building_styles: [
        {
          name: 'interior',
          rooms: [
            {
              name: 'open space',
              filling_types: [
                {
                  name: 'furniture',
                  room_fillings: [
                    {
                      name: 'items',
                       // rendering inventories
                      inventories: [

                        {
                          id: 1,
                          name: 'tv'
                        },
                        {
                          id: 2,
                          name: 'chair'
                        },
                        {
                          id: 3,
                          name: 'table'
                        },
                      ]
                    }
                  ]

                }
              ]
            }
          ]
        },
      ]
    },
  ]
}

This image to understand where I'm getting this saved indexes

enter image description here

I have tried to make recursive function but it only gets first array and doesn't go any further to nested arrays

  const displayBreadCrumbs = () => {
    let menuKeys = Object.keys(menuIndexes)
    console.log({ menuIndexes });
    console.log(file);
    let arr = []
    let i = 0
    let pathToParse = file


    if (i < menuKeys.length) {
      if (menuKeys[i] in pathToParse) {
        pathToParse = pathToParse[menuKeys[i]][menuIndexes[menuKeys[i]]]
        console.log('pathToParse', pathToParse);
        i = i + 1
        // displayBreadCrumbs()
      }
    }
  }
6
  • There is no room_fillings in the sample data (only in the image) so we can't run this as a minimal reproducible example Commented Jun 7, 2020 at 12:54
  • @charlietfl added room_fillings. Also you can get full json in sandbox in data folder Commented Jun 7, 2020 at 13:01
  • Also what is expected result? Get name from each item in the path? Commented Jun 7, 2020 at 13:05
  • @charlietfl yes, get name from each item in the path using given indexes Commented Jun 7, 2020 at 13:09
  • is the result you're after something like [ "office", "interior", "open space", "furniture" ] for the sample data you gave (supposing all indexes are 0 in menuIndexes) Commented Jun 7, 2020 at 13:11

3 Answers 3

2

You can loop through each key-value pair object in menuIndexes as see which key belongs to the current pathToParse object. Once you have the key which applies to the current object, you can access it's associated array as well as the index you need to look at. You can remove the key-value pair from the entries once you've found the key-value pair and again, recursively look for the next key within your new object. You can continue this until you can't find a key from menuIndexes which falls into your current data object (ie: findIndex returns -1);

const pathToParse = { buildings: [ { name: 'office', building_styles: [ { name: 'interior', rooms: [ { name: 'open space', filling_types: [ { name: 'furniture', inventories: [{ id: 1, name: 'tv' }, { id: 2, name: 'chair' }, { id: 3, name: 'table' }, ] } ] } ] }, ] }, ] }

const menuIndexes = {
  buildings: 0,
  building_styles: 0,
  rooms: 0,
  room_fillings: 0,
  filling_types: 0,
}

function getPath(data, entries) {
  const keyIdx = entries.findIndex(([key]) => key in data);
  if(keyIdx <= -1)
    return [];
    
  const [objKey, arrIdx] = entries[keyIdx];
  const obj = data[objKey][arrIdx];
  entries.splice(keyIdx, 1);
  return [obj.name].concat(getPath(obj, entries)); 
}

console.log(getPath(pathToParse, Object.entries(menuIndexes)));

The use of Object.entries() is to search the data object for the key to look at (as we don't want to rely on the key ordering of the menuIndexes object). If you have more control over menuIndexes, you could store it in an array where you can safely rely on the ordering of elements and thus keys:

const pathToParse = { buildings: [ { name: 'office', building_styles: [ { name: 'interior', rooms: [ { name: 'open space', filling_types: [ { name: 'furniture', inventories: [{ id: 1, name: 'tv' }, { id: 2, name: 'chair' }, { id: 3, name: 'table' }, ] } ] } ] }, ] }, ] };

const menuIndexes = [{key: 'buildings', index: 0}, {key: 'building_styles', index: 0}, {key: 'rooms', index: 0}, {key: 'filling_types', index: 0}, {key: 'inventories', index: 0}, ];

function getPath(data, [{key, index}={}, ...rest]) {
  if(!key)
    return [];
  const obj = data[key][index];
  return [obj.name, ...getPath(obj, rest)]; 
}

console.log(getPath(pathToParse, menuIndexes));

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

1 Comment

using Array for menuIndexes (instead of Object) is much smarter. Not only Object is not ordered, but some items in the tree could have a duplicate name, making it impossible to represent all breadcumb paths using an Object this way. Array mitigates both problems :D
1

As usual, I would build such a function atop some reusable parts. This is my approach:

// Utility functions
const path = (obj, names) =>
  names .reduce ((o, n) => (o || {}) [n], obj)

const scan = (fn, init, xs) => 
  xs .reduce (
    (a, x, _, __, n = fn (a .slice (-1) [0], x)) => [...a, n], 
    [init]
  ) .slice (1)
  
const pluck = (name) => (xs) =>
  xs .map (x => x [name])


// Main function
const getBreadCrumbs = (data, indices) => 
  pluck ('name') (scan (path, data, Object .entries (indices)))


// Sample data
const data = {buildings: [{name: "office", building_styles: [{building_style: 0}, {building_style: 1}, {building_style: 2}, {name: "interior", rooms: [{room: 0}, {room: 1}, {name: "open space", filling_types: [{filling_type: 0}, {name: "furniture", room_fillings: [{name: "items", inventories: [{id: 1, name: "tv"}, {id: 2, name: "chair"}, {id: 3, name: "table"}]}, {room_filling: 1}]}, {filling_type: 2}]}, {room: 3}]}, {building_style: 4}]}]}
const menuIndexes = {buildings: 0, building_styles: 3, rooms: 2, filling_types: 1, room_fillings: 0}


// Demo
console .log (getBreadCrumbs (data, menuIndexes))

We have three reusable functions here:

  • path takes an object and a list of node names (strings or integers) and returns the value at the given path or undefined if any node is missing.1 For example:

    path ({a: {b: [{c: 10}, {c: 20}, {c: 30}, {c: 40}]}}, ['a', 'b', 2, 'c']) //=> 30.
    
  • scan is much like Array.prototype.reduce, except that instead of returning just the final value, it returns a list of the values calculated at each step. For example if add is (x, y) => x + y, then:

    scan (add, 0, [1, 2, 3, 4, 5]) //=> [1, 3, 6, 10, 15]
    
  • pluck pulls the named property off each of a list of objects:

    pluck ('b') ([{a: 1, b: 2, c: 3}, {a: 10, b: 20, c: 30}, {a: 100, b: 200, c: 300}]) 
    //=> [2, 20, 200]
    

In practice, I would actually factor these helpers even further, defining path in terms of const prop = (obj, name) => (obj || {}) [name], and using const last = xs => xs.slice (-1) [0] and const tail = (xs) => xs .slice (-1) in defining scan. But that's not important to this problem.

Our main function then can simply use these, along with Object.entries2, to first grab the entries from your index, passing that, our path function, and the data to scan to get a list of the relevant object nodes, then passing the result to pluck along with the string 'name' that we want to extract.

I use path and pluck nearly daily. scan is less common, but it's important enough that it's included in my usual utility libraries. With functions like that easily at hand, it's pretty simple to write something like getBreadCrumbs.


1 Side note, I usually define this as (names) => (obj) => ..., which I find most commonly useful. This form happens to fit better with the code used, but it would be easy enough to adapt the code to my preferred form: instead of scan (path, data, ...), we could just write scan ((a, ns) => path (ns) (a), data, ...)

2 As noted in the answer from Nick Parsons and that comment on it from Thankyou, there is a good argument to be made for storing this information in an array, which is explicitly ordered, rather than depending on the strange and arbitrary ordering that general objects get. If you did that, this code would only change by removing the Object .entries call in the main function.

Comments

0

You can define a recursive function like this, it iterates through menuIndexes keys and find corresponding object in data. Once it finds the data, it pushes the name into output array and calls the function again with this object and menuIndexes

const displayBreadCrumbs = (data, menuIndexes) => {
    const output = [];
    Object.keys(menuIndexes).forEach(key => {
        if (data[key] && Array.isArray(data[key]) && data[key][menuIndexes[key]]) {
            output.push(data[key][menuIndexes[key]].name);
            output.push(...displayBreadCrumbs(data[key][menuIndexes[key]], menuIndexes));
        }
    });
    return output;
};
const data = { buildings: [ { name: 'office', building_styles: [ { name: 'interior', rooms: [ { name: 'open space', filling_types: [ { name: 'furniture', inventories: [{ id: 1, name: 'tv' }, { id: 2, name: 'chair' }, { id: 3, name: 'table' }, ] } ] } ] }, ] }, ] };

const menuIndexes = {
  buildings: 0,
  building_styles: 0,
  rooms: 0,
  room_fillings: 0,
  filling_types: 0,
};

displayBreadCrumbs(data, menuIndexes); // ["office", "interior", "open space", "furniture"]

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.