I want to be able to remove an object from an array of objects that have children. I thought immediately this is a job for recursion, but I am unable to get my recursion function to work properly. I thought of using reduce to rebuild the data structure without the object that I want to remove. The function should accept two parameters the array of nested objects and an Id.
My requirements are: remove the node and all children below.
At first glance, this seems easy, but the challenge I find is removing a child and keeping the entire data structure intact. Deleting the parent by filtering based on the id is trivial, but the nested children pose a problem.
My data structure looks like this:
const data = [{
id: 'BFQEA1W2RK1YRETZ9343',
name: 'Cover',
activityId: 'BFQEA1W2RK1YRETZ9343',
nodeType: 'activity',
suppressed: true,
hidden: true
},
{
children: [
{
id: 'ZNRAE749BSD0CTGHY888',
name: 'Consultants, Reviewers, and National Geographic Exploration',
activityId: 'ZNRAE749BSD0CTGHY888',
nodeType: 'activity',
suppressed: false,
hidden: false
},
{
id: 'JZLS37EVZQM22H9Q4655',
name: 'The National Geographic Approach',
activityId: 'JZLS37EVZQM22H9Q4655',
nodeType: 'activity',
suppressed: false,
hidden: false
},
]
}
]
If I pass this Id(ZNRAE749BSD0CTGHY888) to the function my expected data result should be:
const expected = [{
id: 'BFQEA1W2RK1YRETZ9343',
name: 'Cover',
activityId: 'BFQEA1W2RK1YRETZ9343',
nodeType: 'activity',
suppressed: true,
hidden: true
},
{
children: [
{
id: 'JZLS37EVZQM22H9Q4655',
name: 'The National Geographic Approach',
activityId: 'JZLS37EVZQM22H9Q4655',
nodeType: 'activity',
suppressed: false,
hidden: false
},
]
}
]
My function looks like this:
findNode = (id, arr) => {
return arr.reduce((a, item) => {
// if (item.id === id) {
// console.log('here');
// return item;
// }
if (item.id !== id) {
a.push(item);
}
if (item.children) {
return this.findNode(id, item.children);
}
}, []);
};
The function's reduce accumulator is undefined, but I am unsure why. It should be making a new array. What am I missing here?
In my head, this seems to work, but it fails. Maybe my approach is completely off. How should I go about solving this?
In your code you are not returning the accumulator. That's why you're getting undefined. And there's no reason to recurse over children of items that you don't push, so you should nest the recursion under the if.
You can loop over you root array with reduce(). If the id matches, just return and continue. Other wise you can recursively pass the children the filter and push to the return array:
const data = [{id: 'BFQEA1W2RK1YRETZ9343',name: 'Cover',activityId: 'BFQEA1W2RK1YRETZ9343',nodeType: 'activity',suppressed: true,hidden: true},{children: [{id: 'ZNRAE749BSD0CTGHY888',name: 'Consultants, Reviewers, and National Geographic Exploration',activityId: 'ZNRAE749BSD0CTGHY888',nodeType: 'activity',suppressed: false,hidden: false},{id: 'JZLS37EVZQM22H9Q4655',name: 'The National Geographic Approach',activityId: 'JZLS37EVZQM22H9Q4655',nodeType: 'activity',suppressed: false,hidden: false},]}]
function filterID(id, data) {
return data.reduce((arr, item) => {
if (item.id != id) {
if (item.children) item.children = filterID(id, item.children)
arr.push(item)
}
return arr // <<-- need to return the accumulator
}, [])
}
console.log(filterID("ZNRAE749BSD0CTGHY888", data))
Related
I have a question about working with the find method. I have a task - I need to go through the array and find a match with a specific string. But at the same time there is a condition that this string can be inside one of the objects already in its child array. I make an if construct in my function to check this when passing through the array, but it does not work out as I expected. Tell me, please, where I went wrong.
P.S. I write more correctly. If the array object "newList" has "items" , then you need to look for comparison not in the object, but in its "items" array among "role" . If "items" does not exist for the object, then we look for a match in this object among "role"
const newList = [
{
role: "role111",
title: "title1",
},
{
role: "role222",
title: "title2",
},
{
role: "role333",
title: "title3",
},
{
role: "role444",
title: "title4",
items: [{
role: "role555",
title: "title5",
}, {
role: "role666",
title: "title6",
}, {
role: "role777",
title: "title7",
},]
},
{
role: "role888",
title: "title8",
},
];
const url = "role7";
export const findAfterRefresh = (list, url) =>
list.find((item) => {
if (item.items && item.items?.length > 0) {
return item.items.find((childrenITem) => childrenITem.role.includes(url));
}
return item.role.includes(url);
});
;
findAfterRefresh(newList, url);
Your solution was close, but if you call find on newList, it can only ever return one of the elements of newList, it can't return an element from the items array of one of those elements. That plus the fact you want the role value, not the element itself, makes the find method not a good match for your current data structure (but keep reading; if you really want to use find, there's a way).
Instead, a simple loop with recursion does the job:
/*export*/ const findAfterRefresh = (list, url) => {
for (const item of list) {
if (item.role?.includes(url)) {
return item.role;
}
if (item.items?.length) {
const childRole = findAfterRefresh(item.items, url);
if (childRole) {
return childRole;
}
}
}
};
Here's a version with explanatory comments:
/*export*/ const findAfterRefresh = (list, url) => {
// Loop the given list...
for (const item of list) {
// ...check this item's role
// (Remove v-- this `?` if `role` will always be there)
if (item.role?.includes(url)) {
return item.role;
}
// If this item has child items, check them
if (item.items?.length) {
// Search child items using recursion
const childRole = findAfterRefresh(item.items, url);
if (childRole) {
// Found it, return it
return childRole;
}
// Didn't find it, keep looping
}
}
};
Live Example:
const newList = [
{
role: "role1",
title: "title1",
},
{
role: "role2",
title: "title2",
},
{
role: "role3",
title: "title3",
},
{
role: "role4",
title: "title4",
items: [
{
role: "role5",
title: "title5",
},
{
role: "role6",
title: "title6",
},
{
role: "role7plusotherstuff",
title: "title7",
},
],
},
];
/*export*/ const findAfterRefresh = (list, url) => {
// Loop the given list...
for (const item of list) {
// ...check this item's role
// (Remove v-- this `?` if `role` will always be there)
if (item.role?.includes(url)) {
return item.role;
}
// If this item has child items, check them
if (item.items?.length) {
// Search child items using recursion
const childRole = findAfterRefresh(item.items, url);
if (childRole) {
// Found it, return it
return childRole;
}
// Didn't find it, keep looping
}
}
};
console.log("Searching for 'role7'");
console.log(findAfterRefresh(newList, "role7"));
console.log("Searching for 'role2'");
console.log(findAfterRefresh(newList, "role2"));
Note: I added a bit to the role containing role7 so you could see that the code returns the full role, not just the bit in url.
But if you really want to use find, you can do it by first creating a flat array of roles:
// Creates a new array of `role` values (the array may also contain
// `undefined`, if the `role` property of any element or child element is
// `undefined`)
const flattenRoles = (list) =>
(list ?? []).flatMap((item) => [item.role, ...flattenRoles(item.items)]);
/*export*/ const findAfterRefresh = (list, url) => {
return flattenRoles(list).find((role) => role?.includes(url));
};
That code's a big shorter, but note that it creates a number of temporary arrays, and it always works its way through the full list before looking for roles, whereas the earlier version stops looking as soon as it's found a matching role. That's unlikely to be a problem if newList is of a reasonable size, but it's worth keeping in mind. (I'd probably use the earlier version, not this.)
Here's that in action:
const newList = [
{
role: "role1",
title: "title1",
},
{
role: "role2",
title: "title2",
},
{
role: "role3",
title: "title3",
},
{
role: "role4",
title: "title4",
items: [
{
role: "role5",
title: "title5",
},
{
role: "role6",
title: "title6",
},
{
role: "role7plusotherstuff",
title: "title7",
},
],
},
];
// Creates a new array of `role` values (the array may also contain
// `undefined`, if the `role` property of any element or child element is
// `undefined`)
const flattenRoles = (list) =>
(list ?? []).flatMap((item) => [item.role, ...flattenRoles(item.items)]);
/*export*/ const findAfterRefresh = (list, url) => {
return flattenRoles(list).find((role) => role?.includes(url));
};
console.log("Searching for 'role7'");
console.log(findAfterRefresh(newList, "role7"));
console.log("Searching for 'role2'");
console.log(findAfterRefresh(newList, "role2"));
In a comment you've asked:
ESLint: iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.(no-restricted-syntax)
...how much do you think eslnit is right in this case?
That's up to you. If your target environment is ES2015+, there's no need for regenerator-runtime. As far as I know, there are no major pre-ES2015+ environments (IE11 is obsolete and discontinued). But if you need to support it and want to avoid regenerator-runtime, you can replace the for-of loop with some and assigning to a closed-over variable:
const newList = [
{
role: "role1",
title: "title1",
},
{
role: "role2",
title: "title2",
},
{
role: "role3",
title: "title3",
},
{
role: "role4",
title: "title4",
items: [
{
role: "role5",
title: "title5",
},
{
role: "role6",
title: "title6",
},
{
role: "role7plusotherstuff",
title: "title7",
},
],
},
];
/*export*/ const findAfterRefresh = (list, url) => {
// Loop the given list...
let role;
list.some((item) => {
// ...check this item's role
// (Remove v-- this `?` if `role` will always be there)
if (item.role?.includes(url)) {
role = item.role;
return true;
}
// If this item has child items, check them
if (item.items?.length) {
// Search child items using recursion
const childRole = findAfterRefresh(item.items, url);
if (childRole) {
// Found it, return it
role = childRole;
return true;
}
// Didn't find it, keep looping
}
});
return role;
};
console.log("Searching for 'role7'");
console.log(findAfterRefresh(newList, "role7"));
console.log("Searching for 'role2'");
console.log(findAfterRefresh(newList, "role2"));
The return true; statements tell some that you're done, it can stop looping.
...and the error on childRole from typescript is TS7022: 'childRole' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
The TypeScript error is just because the code doesn't have type annotations, because it was a JavaScript question, not a TypeScript question. :-) If you add the appropriate type annotations, it'll be fine.
As per my understanding, You are trying to filtered out the newList with all the objects includes role7 string in role property either in main object or in the child array objects. If Yes, You have to use Array.filter() method instead of Array.find() as it will only returns the first element in the provided array that satisfies the provided testing function.
Live Demo :
const newList = [
{
role: "role111",
title: "title1",
},
{
role: "role777",
title: "title2",
},
{
role: "role333",
title: "title3",
},
{
role: "role444",
title: "title4",
items: [{
role: "role555",
title: "title5",
}, {
role: "role666",
title: "title6",
}, {
role: "role777",
title: "title7",
},]
},
{
role: "role888",
title: "title8",
},
];
const url = "role7";
const findAfterRefresh = (list, url) =>
list.filter((item) => {
return (item.role.includes(url)) ? item :
item.items = item.items?.filter((childrenITem) => childrenITem.role.includes(url));
});
console.log(findAfterRefresh(newList, url));
After reading some of the other solutions I was able to refactor my code down a little bit. I decided to convert the raw array value into a 2d array, each nested array value would hold the role and item values.
I chose to do things this way so that the output of this function can still be used for any purpose regarding the roles and titles of these items. and to create an array which would be easy to iterate through.
const url = "role6";
const findAfterRefresh = (list) =>{
let foundData = []
list.find((item) => {
foundData.push([item.role, item.title]);
if (item.items !== undefined) {
item.items.find((item) => {
foundData.push([item.role, item.title]);
})
}
});
return foundData
}
for(let v in findAfterRefresh(newList) ){
if (findAfterRefresh(newList)[v].includes(url)) {
console.log(findAfterRefresh(newList)[v].includes(url)); break
}
}
//output of findAfterRefresh: [[role1,title1],[role2,title2][... etc
//ouput of the for loop at the bottom: true...
Ok I got to it.
Edit: I didn't read the title carefully, the code at the bottom is with for instead of "find" I hope it's not a problem
const newList = [
{
role: "role1",
title: "title1",
},
{
role: "role2",
title: "title2",
},
{
role: "role3",
title: "title3",
},
{
role: "role4",
title: "title4",
items: [{
role: "role5",
title: "title5",
}, {
role: "role6",
title: "title6",
}, {
role: "role7",
title: "title7",
},]
}
];
const url = "role2";
const findAfterRefresh = (list, url) => {
for(const singleItem of Object.values(list)) {
if(singleItem.items && singleItem.items?.length > 0) {
return singleItem.items.find(child => child.role === url);
};
if(!singleItem.role.includes(url)) {
continue;
};
return singleItem;
}
};
findAfterRefresh(newList, url);
Another question about recursive function, I cant get my head arround them.
I have a list with groups that can have any depth, an example:
{
Id: 1,
Name:"Root",
Children: [
{
Id: 1,
Name:"",
Children: [
{
Id: 1,
Name:"",
Children: [
{
Id: 1,
Name:"",
Children: []
},
]
},
]
},
{
Id: 2,
Name:"",
Children: []
},
{
Id: 3,
Name:"",
Children: []
},
]
}
I show these groups in a dropdown that the user can select.
What I need to do is when the user clicks on any group, I need to show all users that are a part of that group AND its subgroups.
The information about which users belong to the group and its subgroups is hold by the userlist. That list is flat and every user has an prop that contains an membership array.
I have re-written this method below several times, this is the closest I get, but this more than doubles the expected lenght because I get dublicates.
const getAllUsersInGroup = (group, usersFiltered) => {
if (!group.Children.length) return usersFiltered.flat();
return group.Children.flatMap((g) => {
return getAllUsersInGroup(
g,
[...usersFiltered, users.filter((u) => u.Memberships.some((m) => m.GroupId === g.Id))]
);
});
};
Another test returns almost all but there is missing users on bigger groups with many subgroups.
const getAllUsersInGroup = (group, userss) => {
if (!group.Children.length) return [...userss].flat();
return group.Children.flatMap((g) => {
return getAllUsersInGroup(g,
users.filter((u) => u.Memberships.some((m) => m.GroupId === g.Id)),
);
});
};
I must be stuck in some wrong thinking or just pure stupid..
Maybe I dont need to check the Children lenght and just go thro them all, but as I understand it you need some statment that stops the method.
A little help would be much appreciated!
Regards
I am trying to achieve something a bit complex.
I am working on a grouping feature where I have to get the items containing the key checked: true then I need to put them together within the key/array named questionGroup in the first item in the array with checked: true and then deleting the other items that were grouped.
See the structure of the data:
const dummyQuestions = [
{
id: 1,
checked: false,
question: {...},
questionGroup: []
},
{
id: 2,
checked: true,
question: {...},
questionGroup: []
},
{
id: 3,
checked: false,
question: {...},
questionGroup: []
},
{
id: 4,
checked: true,
question: {...},
questionGroup: []
},
{
id: 5,
checked: true,
question: {...},
questionGroup: []
}
];
So lets say you clicked on a certain button which fires a grouping function, as you noticed index 1, 3 and 4 have checked: true, so this must happen:
const dummyQuestions = [
{
id: 1,
checked: false,
question: {...},
questionGroup: []
},
{
id: 2,
checked: true,
question: {...},
questionGroup: [
{
id: 2,
checked: true,
question: {...},
questionGroup: []
},
{
id: 4,
checked: true,
question: {...},
questionGroup: []
},
{
id: 5,
checked: true,
question: {...},
questionGroup: []
}
]
},
{
id: 3,
checked: false,
question: {...},
questionGroup: []
}
];
That's how the data must be after grouping it.
Do you get the idea?
I created a reproducible demo => https://codesandbox.io/s/ts-react-grouping-odxs1?file=/src/App.tsx
EDIT
This is the relevant code:
const handleGroupQuestions = (): void => {
const questionAddedCopy: VariableConfig[] = [...questionAdded];
// Only the questions with checked === true will be grouped.
const filteredCopy: VariableConfig[] = questionAddedCopy.filter(
(q: VariableConfig) => q.checked === true
);
const grouped: VariableConfig[] = [
...questionAdded.filter((q: VariableConfig) => filteredCopy.includes(q))
];
for (let i = 0; i < grouped.length; i++) {
// Here I compare the ids and I put them together into the key questionGroup
if (filteredCopy[i].id === grouped[i].id) {
// I had to set any instead of VariableConfig type because otherwise TS throws an error
addQuestion((q: any) => {
const questionsCopy = [...q];
const mergedQuestions: VariableConfig[] = [
...questionsCopy,
{ ...grouped[i], questionGroup: grouped }
];
// With this `unique` function what I am trying to achieve (which I am not) is
// to have a unique ID per every question/item in the array.
// I need to add the questions to questionGroup to the highest item containing check = true
// I need to delete from the main array, the items going inside questionGroup.
const unique = [
...mergedQuestions.filter((c) => c.id !== questionsCopy[i].id)
];
return unique;
});
break;
}
}
};
And to explain more in deep and to answer your questions.
We see that the first item in the array with checked: true is the item with the id 2, so that will be the place where I have to stored the other items with checked: true because that is the highest item in the array, then I need to delete from the array the items that were grouped into questionGroup.
The id 2 in this case is repeated and that is a requirement, because one id is for the parent and the other one within questionGroup is its own child, get it? Like if it is storing itself into questionGroup.
This is just a grouping functionality where I am saving some steps. Imagine every item contains a checkbox, so the items that you want to group, you have to check them first, then click the GROUP THEM button, the checked items disappear and then get together into the highest checked index, keeping its id and storing itself as well.
Take a look at the arrays I posted. There you can see what is the exact output I need.
Here is one technique:
// helper function
const addChecked = (q, qs) =>
({...q, questionGroup: qs .filter (p => p .checked)})
// main function
const nestChecked = (qs, found = false) =>
qs .flatMap (q => q .checked ? found ? [] : (found = true, [addChecked (q, qs)]) : [q])
// sample data
const dummyQuestions = [{id: 1, checked: false, question: 'foo', questionGroup: []}, {id: 2, checked: true, question: 'bar', questionGroup: []}, {id: 3, checked: false, question: 'baz', questionGroup: []}, {id: 4, checked: true, question: 'qux', questionGroup: []}, {id: 5, checked: true, question: 'corge', questionGroup: []}]
// demo
console .log (JSON .stringify (nestChecked (dummyQuestions), null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
We have a simple helper function that groups all the checked elements of the input into (a copy of) a specific question.
Our main function iterates over the questions with .flatMap, which allows us to do a filter and map in a single traversal. We maintain a flag, found which tells us if we've already handled the checked elements. For each question, if it's not checked, we simply include it, by returning it wrapped in an array, as [q] from the flatMap callback. If it is checked, we evaluate the found flag. If that is set to true, then we return and empty array. If it's not, we return the result of calling our helper function above, again wrapped in an array. (These array wrappers are not specifically needed for this problem; but I like to return a consistent type in the flatMap callback.)
We could easily inline that helper function, as it's only called once. In fact that's how I originally wrote it. But it feels cleaner to me separated.
It's not clear to me if there is some iterative process which will then further nest these. If so, you might have to do something more sophisticated in the helper function dealing wit already-populated questionGroup fields. But that would probably be for a different question.
Finally, we also might want to avoid that default parameter. There are sometimes good reasons to want to do so. I leave this change as an exercise for the reader. :-)
I have an array with objects that I want to filter according to an indefinite number of conditions passed as parameters.
Here's how I filter the array a with a array of conditions in hide ()
const statuses = [
{
id: 0,
name: 'archived'
},
{
id: 1,
name: 'coming',
hide: (...filterParam) => filterParam.every(rule => rule)
}
];
const filteredStatuses = statuses.filter(element => {
switch (element.id) {
case 1:
return !element.hide(this.isTopTabs());
// other cases with others logic
default:
return true;
}
});
Now if each object can have its own children object array like that:
const statuses = [
{
id: 'statuses',
name: 'treeName',
children: [
{
id: 1,
name: 'inProgress',
hide: (...filterParam) => filterParam.every(Boolean)
},
{
id: 2,
name: 'coming',
checked: false,
hide: (...filterParam) => filterParam.every(Boolean)
}
]
}
];
How I can recursively iterate the same way?
Do you have a better way to filter this array dynamically by avoiding the switch / case?
And finally how to type the rest parameters with a generic like that hide: <T>(...filterParam: Array<T>) => filterParam.every(Boolean) ?
How I can recursively iterate the same way?
const filterRecursively = elements => filterStatuses(elements).map(it => ({ ...it, children: it.children && filterRecursively(it.children) }));
Do you have a better way to filter this array dynamically by avoiding the switch / case?
Why? Whats wrong with the switch case? Isn't it fancy enough?
And finally how to type the rest parameters with a generic like that hide: (...filterParam: Array) => filterParam.every(Boolean) ?
<T>(...filterParam: Array<T>) => boolean
I am trying to build tree array from flat array, each item in the flat array has two property need to be used to build the tree array, they are 1. category. 2. subCategrie which is array of string.
let data = [
{
id: 1,
name: "Zend",
category: "php",
subCategory: ["framework"]
},
{
id: 2,
name: "Laravel",
category: "php",
subCategory: ["framework"]
},
{
id: 3,
name: "Vesion 5",
category: "php",
subCategory: ["versions"]
},
{
id: 4,
name: "Angular",
category: "frontend",
subCategory: ["framework", "typescript"]
},
{
id: 5,
name: "Aurelia",
category: "frontend",
subCategory: ["framework", "typescript"]
},
{
id: 6,
name: "JQuery",
category: "frontend",
subCategory: []
}
];
It should be
let tree = [
{
name: "php",
children: [
{
name: "framework",
children: [
{
id: 1,
name: "Zend"
},
{
id: 2,
name: "Laravel"
}
]
},
{
name: "versions",
children: [
{
id: 3,
name: "Vesion 5"
}
]
}
]
}
// ...
];
Is there any article, link solving similar problem?
I gave it many tries but stuck when trying to build the sub categories children.
Here's my last attempt which throws error and I know it's wrong but it's for the ones who want to see my attempts
const list = require('./filter.json')
let tree = {};
for (let filter of list) {
if (tree[filter.category]) {
tree[filter.category].push(filter);
} else {
tree[filter.category] = [filter];
}
}
function buildChildren(list, subcategories, category, index) {
let tree = {}
for (let filter of list) {
if (filter.subcategory.length) {
for (let i = 0; i < filter.subcategory.length; i++) {
let branch = list.filter(item => item.subcategory[i] === filter.subcategory[i]);
branch.forEach(item =>{
if (tree[filter.subcategory[i]]){
tree[filter.subcategory[i]] = tree[filter.subcategory[i]].push(item)
}else{
tree[item.subcategory[i]] = [item]
}
})
}
}
}
console.log('tree ', tree);
}
Heads up, For javascript I usually use Lodash (usually written as _ in code) but most of these methods should also be built in to the objects in javascript (i.e. _.forEach = Array.forEach())
const tree = [];
// First Group all elements of the same category (PHP, Frontend, etc.)
data = _.groupBy(data, 'category');
_.forEach(data, function (categoryElements, categoryName) {
// Each Category will have it's own subCategories that we will want to handle
let categorySubCategories = {};
// The categoryElements will be an array of all the objects in a given category (php / frontend / etc..)
categoryElements.map(function (element) {
// For each of these categoryies, we will want to grab the subcategories they belong to
element.subCategory.map(function (subCategoryName) {
// Check if teh category (PHP) already has already started a group of this subcategory,
// else initialize it as an empty list
if (!categorySubCategories[subCategoryName]) { categorySubCategories[subCategoryName] = []; }
// Push this element into the subcategory list
categorySubCategories[subCategoryName].push({id: element.id, name: element.name});
});
});
// Create a category map, which will be a list in the format {name, children}, created from
// our categorySubCategories object, which is in the format {name: children}
let categoryMap = [];
_.forEach(categorySubCategories, function (subCategoryElements, subCategoryName) {
categoryMap.push({name: subCategoryName, children: subCategoryElements});
});
// Now that we've grouped the sub categories, just give the tree it's category name and children
tree.push({name: categoryName, children: categoryMap});
});
};
The key to success here is to create an interim format that allows for easy lookups. Because you work with children arrays, you end up having to use filter and find whenever you add something new, to prevent duplicates and ensure grouping.
By working with a format based on objects and keys, it's much easier to do the grouping.
We can create the groups in a single nested loop, which means we only touch each item once for the main logic. The group has this format:
{ "categoryName": { "subCategoryName": [ { id, name } ] } }
Then, getting to the required { name, children } format is a matter of one more loop over the entries of this tree. In this loop we move from { "categoryName": catData } to { name: "categoryName", children: catData }
Here's an example that shows the two steps separately:
const data=[{id:1,name:"Zend",category:"php",subCategory:["framework"]},{id:2,name:"Laravel",category:"php",subCategory:["framework"]},{id:3,name:"Vesion 5",category:"php",subCategory:["versions"]},{id:4,name:"Angular",category:"frontend",subCategory:["framework","typescript"]},{id:5,name:"Aurelia",category:"frontend",subCategory:["framework","typescript"]},{id:6,name:"JQuery",category:"frontend",subCategory:[]}];
// { category: { subCategory: [ items ] } }
const categoryOverview = data.reduce(
(acc, { id, name, category, subCategory }) => {
// Create a top level group if there isn't one yet
if (!acc[category]) acc[category] = {};
subCategory.forEach(sc => {
// Create an array for this subCat if there isn't one yet
acc[category][sc] = (acc[category][sc] || [])
// and add the current item to it
.concat({ id, name });
});
return acc;
},
{}
)
const nameChildrenMap = Object
.entries(categoryOverview)
// Create top level { name, children } objects
.map(([cat, subCats]) => ({
name: cat,
children: Object
.entries(subCats)
// Create sub level { name, children } objects
.map(([subCat, items]) => ({
name: subCat,
children: items
}))
}))
console.log(nameChildrenMap);