Reputation: 2449
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
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()
}
}
}
Upvotes: 0
Views: 445
Reputation: 50797
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.entries
2, 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.
Upvotes: 1
Reputation: 50759
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));
Upvotes: 2
Reputation: 4770
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"]
Upvotes: 0