I'm a learner developer, and I'm build a app with a tree menu(react + redux + sagas), but I'm getting some errors of Mutation State, I saw what best practices is stay de state flat as possible, but I didn't finded one menu tree what work with a flat state, so my data is look this:
menuTree: [{
id: 'id-root',
name: 'root',
toggled: true,
children: [
{
id: 'id-parent1',
name: 'parent1',
toggled: true,
children: [
{
id: '123',
name: 'parent1_child1'
},
{
id: '234',
name: 'parent1_child2'
}
]
},
{
id: 'id-loading-parent',
name: 'loading parent',
loading: true,
children: []
},
{
id: 'id-parent2',
name: 'parent2',
toggled: true,
children: [
{
id: 'parent2_children1',
name: 'nested parent2',
children: [
{
id: '345',
name: 'parent2 child 1 nested child 1'
},
{
id: '456',
name: 'parent2 child 1 nested child 2'
}
]
}
]
}
]
}],
And my redux action:
case types.SOLUTION__MENUCURSOR__SET:
// console.log('action payload', action.payload);
// console.log('state', state);
const cursor = action.payload.cursor;
// console.log('set menu cursor action', cursor);
return {
...state,
menuTree: state.menuTree.map(
function buscaIdMenuTree(currentValue, index, arr){
if(currentValue.id){
if(currentValue.id.includes(cursor.id)){
currentValue.toggled = action.payload.toggled;
return arr;
}else{
if(currentValue.children)
{
currentValue.children.forEach(function(currentValue, index, arr){
return buscaIdMenuTree(currentValue, index, arr);
});
}
}
return arr;
}
}
)[0]
};
The code works but I get Mutation State Error, so someone can help me to fix it ?
You can rebuild your menu as a plain list:
let menuTree = [{
id: 'id-root',
name: 'root',
toggled: true,
parent: null
},{
id: 'id-parent1',
name: 'parent1',
toggled: true,
parent: 'id-root'
},{
id: '123',
name: 'parent1_child1',
parent: 'id-parent1'
},{
id: '234',
name: 'parent1_child1',
parent: 'id-parent1'
},
{
id: 'id-loading-parent',
name: 'loading parent',
loading: true,
parent: 'id-root'
},{
id: 'id-parent2',
name: 'parent2',
toggled: true,
parent: 'id-root'
},{
id: 'parent2_children1',
name: 'nested parent2',
parent: 'id-parent2'
},{
id: '345',
name: 'parent2 child 1 nested child 1',
parent: 'parent2_children1'
},
{
id: '456',
name: 'parent2 child 1 nested child 2',
parent: 'parent2_children1'
}]
then if your menu renderer require a tree you can convert the list to a tree so inside the component renderer this.menuTree will be a tree:
const buildTree = (tree, cParent = null) => {
return tree.filter(cNode => cNode.parent == cParent).reduce((curr, next) => {
let cNode = {...next, children: buildTree(tree, next.id)}
delete cNode.parent
return [...curr, cNode]
}, [])
}
function mapStateToProps(state) {
return {
mapTree: builTree(state.mapTree)
};
}
export default connect(mapStateToProps)(YourComponent);
Inside the mutation now you just need to create a list of node that needs to be toggled and then map the state accordingly
case types.SOLUTION__MENUCURSOR__SET:
// console.log('action payload', action.payload);
// console.log('state', state);
const cursor = action.payload.cursor;
// console.log('set menu cursor action', cursor);
const getToggleList = (tree, cursor) => {
let target = tree.find(cNode => cNode.id == cursor.id)
if(target.parent != null){
let parent = tree.find(cNode => cNode.id == target.parent)
return [target.parent, ...getToggleList(tree, parent)]
}else{
return []
}
}
let toggleList = [cursor.id, ...getToggleList(state.menuTree, cursor.id)]
return {
...state,
menuTree: state.menuTree.map(node => ({...node, toggle: toggleList.includes(node.id)}))
};
Related
How do I remove an element from a JavaScript object by ID?
For instance I have to remove 004 or 007:
const obj = {
id: '001',
children: [
{
id: '002',
children: [
{
id: '003',
children: [],
},
{
id: '004',
children: [
{
id: '005',
children: [],
}
],
}
],
},
{
id: '006',
children: [
{
id: '007',
children: [],
}
],
},
]
}
i am trying to like this, find id but what should be next. It is expected to remove id from the object.
const removeById = (obj = {}, id = '') => {
console.log('obj: ', obj)
const search = obj.children.find(o => o.id === id)
console.log('##search: ', search)
if(search) {
console.log('## parent id: ', obj.id)
...
}
if (obj.children && obj.children.length > 0) {
obj.children.forEach(el => removeById(el, id));
}
}
removeById(obj, '007')
You can use findIndex to get the location in the array.
To remove an element from an array you need to use splice.
You can then loop over the children with some and check the children's children. Using some, you can exit out when you find the id so you do not have to keep looping.
let obj = {
id: '001',
children: [
{
id: '002',
children: [
{
id: '003',
children: [],
},
{
id: '004',
children: [
{
id: '005',
children: [],
}
],
}
],
},
{
id: '006',
children: [
{
id: '007',
children: [],
}
],
},
]
}
const removeById = (parentData, removeId) => {
// This is only the parent level!
// If you will never delete the first level parent, this is not needed
if (parentData.id === removeId){
Object.keys(data).forEach(key => delete parentData[key]);
return true;
}
function recursiveFind (children) {
// check if any of the children have the id
const index = children.findIndex(({id}) => id === removeId);
// if we have an index
if (index != -1) {
// remove it
children.splice(index, 1);
// say we found it
return true;
}
// Loop over the chldren check their children
return children.some(child => recursiveFind(child.children));
}
return recursiveFind(parentData.children);
}
removeById(obj,'004');
removeById(obj,'007');
console.log(obj)
We can use a recursive function to do it
let obj = {
id: '001',
children: [
{
id: '002',
children: [
{
id: '003',
children: [],
},
{
id: '004',
children: [
{
id: '005',
children: [],
}
],
}
],
},
{
id: '006',
children: [
{
id: '007',
children: [],
}
],
},
]
}
const removeById = (data,id) =>{
if(data.id === id){
delete data.id
delete data.children
return
}
data.children.forEach(d =>{
removeById(d,id)
})
}
removeById(obj,'006')
console.log(obj)
Update: not leave an empty object after removing
let obj = {
id: '001',
children: [
{
id: '002',
children: [
{
id: '003',
children: [],
},
{
id: '004',
children: [
{
id: '005',
children: [],
}
],
}
],
},
{
id: '006',
children: [
{
id: '007',
children: [],
}
],
},
]
}
let match = false
const removeById = (data,id) =>{
match = data.some(d => d.id == id)
if(match){
data = data.filter(d => d.id !== id)
}else{
data.forEach(d =>{
d.children = removeById(d.children,id)
})
}
return data
}
let data = [obj]
console.log(removeById(data,'004'))
I have a tree structure like this:
const tree = [
{
slug: 'item-1',
children: [
{
slug: 'item-1-1',
children: [
{ slug: 'item-1-1-1' },
{ slug: 'item-1-1-2' }
],
},
],
},
{
slug: 'item-2',
children: [
{
slug: 'item-2-1',
children: [
{ slug: 'item-2-1-1' },
{ slug: 'item-2-1-2' }
],
},
],
},
];
I want to filter it based on the slug which is not hard. I've seen some recursive solutions on StackOverflow. But I want only the direct children of the item to be in the result. For example, if I search for slug === "item-1", the result should be:
[
{
slug: "item-1"
children: [
slug: "item-1-1"
],
},
]
Maybe I can combine filter() and reduce() or even map() and somehow solve this but it doesn't seem optimal. The tree is likely to be big and complex. How would you solve this?
Here's a recursive approach. Basically, we are doing a breadth-first search, firstly we search at a level, and if the required item is found return the result after modifying the children array. If the key is not found in that level search the children.
const tree = [
{
slug: 'item-1',
children: [
{
slug: 'item-1-1',
children: [{ slug: 'item-1-1-1' }, { slug: 'item-1-1-2' }],
},
],
},
{
slug: 'item-2',
children: [
{
slug: 'item-2-1',
children: [{ slug: 'item-2-1-1' }, { slug: 'item-2-1-2' }],
},
],
},
];
const search = (data, key) => {
if (!data?.length) return null;
const children = [];
let found = null;
data.forEach((obj) => {
if (obj.slug === key) {
found = obj;
}
children.push(...(obj.children || []));
});
return found
? {
slug: key,
...(found.children?.length && {
children: found.children.map(({ slug }) => ({ slug })),
}),
}
: search(children, key);
};
console.log(search(tree, 'item-1-1'));
Recently, I gave an interview and rejected there were about 10 questions. Each question had 60 seconds. There was a question that went wrong but I was curious why it happened.
I have to filter those objects in which given SearchValue match with the list array object name property. The search value was already given.
For example:
const SearchValue = 'event';
It is filtered array because list[0].name property value match with the the event text.
const res = [
{
id: 2,
name: 'Events',
list: [
{
id: 1,
name: 'Event Ticketing System',
slug: '/'
},
{
id: 2,
name: 'Events Management Software',
slug: '/'
}
]
}
];
The name property contains value something like this Online Translation Services and Spelling and Grammar Check etc. If the search value match with the text then save it those filtered objects and console.log. The data-set was something like this.
const listing = [
{
id: 1,
name: 'Language',
list: [
{
id: 1,
name: 'Online Translation Services',
slug: '/'
},
{
id: 2,
name: 'Spelling and Grammar Check',
slug: '/'
},
{
id: 3,
name: 'TEFL Courses',
slug: '/'
},
{
id: 4,
name: 'Language Learning',
slug: '/'
}
]
},
{
id: 2,
name: 'Events',
list: [
{
id: 1,
name: 'Event Ticketing System',
slug: '/'
},
{
id: 2,
name: 'Events Management Software',
slug: '/'
}
]
}
];
My implementation was this.
const SearchValue = 'event';
const listing = [{
id: 1,
name: 'Language',
list: [{
id: 1,
name: 'Online Translation Services',
slug: '/'
},
{
id: 2,
name: 'Spelling and Grammar Check',
slug: '/'
},
{
id: 3,
name: 'TEFL Courses',
slug: '/'
},
{
id: 4,
name: 'Language Learning',
slug: '/'
}
]
},
{
id: 2,
name: 'Events',
list: [{
id: 1,
name: 'Event Ticketing System',
slug: '/'
},
{
id: 2,
name: 'Events Management Software',
slug: '/'
}
]
}
];
const res = listing.filter(object => {
if (Array.isArray(object.list) && object.list.length > 0) {
return object.list.filter((item) => {
return item.name.toLowerCase().indexOf(SearchValue.toLowerCase()) !== -1;
});
}
});
console.log('Result Array', res);
If anyone can provide a good solution really appreciated. I also want to know What was wrong with this logic?
You can use Array#filter along with Array#some to verify if any of the elements of the list property of each object contains the search text in its name.
const listing = [ { id: 1, name: 'Language', list: [ { id: 1, name: 'Online Translation Services', slug: '/' }, { id: 2, name: 'Spelling and Grammar Check', slug: '/' }, { id: 3, name: 'TEFL Courses', slug: '/' }, { id: 4, name: 'Language Learning', slug: '/' } ] }, { id: 2, name: 'Events', list: [ { id: 1, name: 'Event Ticketing System', slug: '/' }, { id: 2, name: 'Events Management Software', slug: '/' } ] } ];
const SearchValue = 'event';
const res = listing.filter(({list})=>
list.some(({name})=>name.toLowerCase().includes(SearchValue.toLowerCase())));
console.log(res);
Check with RegEx and test
const SearchValue = 'event';
const listing = [{
id: 1,
name: 'Language',
list: [{
id: 1,
name: 'Online Translation Services',
slug: '/'
},
{
id: 2,
name: 'Spelling and Grammar Check',
slug: '/'
},
{
id: 3,
name: 'TEFL Courses',
slug: '/'
},
{
id: 4,
name: 'Language Learning',
slug: '/'
}
]
},
{
id: 2,
name: 'Events',
list: [{
id: 1,
name: 'Event Ticketing System',
slug: '/'
},
{
id: 2,
name: 'Events Management Software',
slug: '/'
}
]
}
];
const res = listing.filter(object => {
if (Array.isArray(object.list) && object.list.length > 0) {
// Check items with RegEx test
// and update object.list
object.list = object.list.filter(item => new RegExp(SearchValue, 'i').test(item.name));
// Return true if list length > 0
if(object.list.length > 0) return true;
}
});
console.log('Result Array', res);
Array Filter
let newArray = arr.filter(callback(element[, index, [array]])[, thisArg])
callback
Function is a predicate, to test each element of the array. Return true to keep the element, false otherwise.
Return value
A new array with the elements that pass the test. If no elements pass the test, an empty array will be returned.
const SearchValue = 'language';
const listing = [{
id: 1,
name: 'Language',
list: [{
id: 1,
name: 'Online Translation Services',
slug: '/'
},
{
id: 2,
name: 'Spelling and Grammar Check',
slug: '/'
},
{
id: 3,
name: 'TEFL Courses',
slug: '/'
},
{
id: 4,
name: 'Language Learning',
slug: '/'
}
]
},
{
id: 2,
name: 'Events',
list: [{
id: 1,
name: 'Event Ticketing System',
slug: '/'
},
{
id: 2,
name: 'Events Management Software',
slug: '/'
}
]
}
];
const res = listing.reduce((pre, object) => {
if(!Array.isArray(object.list) || object.list.length <= 0)
return pre
object.list = object.list.filter((item) => item.name.toLowerCase().indexOf(SearchValue.toLowerCase()) > -1);
return object.list.length > 0 ? [...pre, object] : pre;
}, []);
console.log('Result Array', res);
I am trying to build a tree recursively from an array of objects. I am currently using the reduce() method to iterate through the items in the array and figure out which children belong to a particular item and populating it, then recursively populating the children of those children and so on. However, I have been unable to take the last nodes(e.g persian and siamese in this case) and put them in array(see expected and current output below)
let categories = [
{ id: 'animals', parent: null },
{ id: 'mammals', parent: 'animals' },
{ id: 'cats', parent: 'mammals' },
{ id: 'dogs', parent: 'mammals' },
{ id: 'chihuahua', parent: 'dogs' },
{ id: 'labrador', parent: 'dogs' },
{ id: 'persian', parent: 'cats' },
{ id: 'siamese', parent: 'cats' }
];
const reduceTree = (categories, parent = null) =>
categories.reduce(
(tree, currentItem) => {
if(currentItem.parent == parent){
tree[currentItem.id] = reduceTree(categories, currentItem.id);
}
return tree;
},
{}
)
console.log(JSON.stringify(reduceTree(categories), null, 1));
expected output:
{
"animals": {
"mammals": {
"cats": [ // <-- an array of cat strings
"persian",
"siamese"
],
"dogs": [ // <-- an array of dog strings
"chihuahua",
"labrador"
]
}
}
}
current output:
{
"animals": {
"mammals": {
"cats": { // <-- an object with cat keys
"persian": {},
"siamese": {}
},
"dogs": { // <-- an object with dog keys
"chihuahua": {},
"labrador": {}
}
}
}
}
How should I go about solving the problem?
I put a condition to merge the result as an array if a node has no child. Try this
let categories = [
{ id: 'animals', parent: null },
{ id: 'mammals', parent: 'animals' },
{ id: 'cats', parent: 'mammals' },
{ id: 'dogs', parent: 'mammals' },
{ id: 'chihuahua', parent: 'dogs' },
{ id: 'labrador', parent: 'dogs' },
{ id: 'persian', parent: 'cats' },
{ id: 'siamese', parent: 'cats' }
];
const reduceTree = (categories, parent = null) =>
categories.reduce(
(tree, currentItem) => {
if(currentItem.parent == parent){
let val = reduceTree(categories, currentItem.id);
if( Object.keys(val).length == 0){
Object.keys(tree).length == 0 ? tree = [currentItem.id] : tree.push(currentItem.id);
}
else{
tree[currentItem.id] = val;
}
}
return tree;
},
{}
)
console.log(JSON.stringify(reduceTree(categories), null, 1));
NOTE: if your data structure changes again this parser might fail for some other scenarios.
Here is a solution without recursion:
const categories = [{ id: 'animals', parent: null },{ id: 'mammals', parent: 'animals' },{ id: 'cats', parent: 'mammals' },{ id: 'dogs', parent: 'mammals' },{ id: 'chihuahua', parent: 'dogs' },{ id: 'labrador', parent: 'dogs' },{ id: 'persian', parent: 'cats' },{ id: 'siamese', parent: 'cats' }];
// Create properties for the parents (their values are empty objects)
let res = Object.fromEntries(categories.map(({parent}) => [parent, {}]));
// Overwrite the properties for the parents of leaves to become arrays
categories.forEach(({id, parent}) => res[id] || (res[parent] = []));
// assign children to parent property, either as property of parent object or as array entry in it
categories.forEach(({id, parent}) => res[parent][res[id] ? id : res[parent].length] = res[id] || id);
// Extract the tree for the null-entry:
res = res.null;
console.log(res);
I have the following array (that's actually coming from a backend service):
const flat: Item[] = [
{ id: 'a', name: 'Root 1', parentId: null },
{ id: 'b', name: 'Root 2', parentId: null },
{ id: 'c', name: 'Root 3', parentId: null },
{ id: 'a1', name: 'Item 1', parentId: 'a' },
{ id: 'a2', name: 'Item 1', parentId: 'a' },
{ id: 'b1', name: 'Item 1', parentId: 'b' },
{ id: 'b2', name: 'Item 2', parentId: 'b' },
{ id: 'b2-1', name: 'Item 2-1', parentId: 'b2' },
{ id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },
{ id: 'b3', name: 'Item 3', parentId: 'b' },
{ id: 'c1', name: 'Item 1', parentId: 'c' },
{ id: 'c2', name: 'Item 2', parentId: 'c' }
];
where Item is:
interface Item {
id: string;
name: string;
parentId: string;
};
In order to be compatible with a component that displays a tree (folder like) view, it needs to be transformed into:
const treeData: NestedItem[] = [
{
id: 'a',
name: 'Root 1',
root: true,
count: 2,
children: [
{
id: 'a1',
name: 'Item 1'
},
{
id: 'a2',
name: 'Item 2'
}
]
},
{
id: 'b',
name: 'Root 2',
root: true,
count: 5, // number of all children (direct + children of children)
children: [
{
id: 'b1',
name: 'Item 1'
},
{
id: 'b2',
name: 'Item 2',
count: 2,
children: [
{ id: 'b2-1', name: 'Item 2-1' },
{ id: 'b2-2', name: 'Item 2-2' },
]
},
{
id: 'b3',
name: 'Item 3'
},
]
},
{
id: 'c',
name: 'Root 3',
root: true,
count: 2,
children: [
{
id: 'c1',
name: 'Item 1'
},
{
id: 'c2',
name: 'Item 2'
}
]
}
];
where NestedItem is:
interface NestedItem {
id: string;
name: string;
root?: boolean;
count?: number;
children?: NestedItem[];
}
All I've tried so far is something like:
// Get roots first
const roots: NestedItem[] = flat
.filter(item => !item.parentId)
.map((item): NestedItem => {
return { id: item.id, name: item.name, root: true }
});
// Add "children" to those roots
const treeData = roots.map(node => {
const children = flat
.filter(item => item.parentId === node.id)
.map(item => {
return { id: item.id, name: item.name }
});
return {
...node,
children,
count: node.count ? node.count + children.length : children.length
}
});
But this only gets the first level of children, of course (direct children of root nodes). It somehow needs to be recursive, but I have no idea how to accomplish that.
Making no assumptions about the order of the flattened array or how deep a nested object can go:
Array.prototype.reduce is flexible enough to get this done. If you are not familiar with Array.prototype.reduce I recommend reading this. You could accomplish this by doing the following.
I have two functions that rely on recursion here: findParent and checkLeftOvers. findParent attempts to find the objects parent and returns true or false based on whether it finds it. In my reducer I add the current value to the array of left overs if findParent returns false. If findParent returns true I call checkLeftOvers to see if any object in my array of left overs is the child of the object findParent just added.
Note: I added { id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'} to the flat array to demonstrate that this will go as deep as you'd like. I also reordered flat to demonstrate that this will work in that case as well. Hope this helps.
const flat = [
{ id: 'a2', name: 'Item 1', parentId: 'a' },
{ id: 'b2-2-1', name: 'Item 2-2-1', parentId: 'b2-2'},
{ id: 'a1', name: 'Item 1', parentId: 'a' },
{ id: 'a', name: 'Root 1', parentId: null },
{ id: 'b', name: 'Root 2', parentId: null },
{ id: 'c', name: 'Root 3', parentId: null },
{ id: 'b1', name: 'Item 1', parentId: 'b' },
{ id: 'b2', name: 'Item 2', parentId: 'b' },
{ id: 'b2-1', name: 'Item 2-1', parentId: 'b2' },
{ id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },
{ id: 'b3', name: 'Item 3', parentId: 'b' },
{ id: 'c1', name: 'Item 1', parentId: 'c' },
{ id: 'c2', name: 'Item 2', parentId: 'c' }
];
function checkLeftOvers(leftOvers, possibleParent){
for (let i = 0; i < leftOvers.length; i++) {
if(leftOvers[i].parentId === possibleParent.id) {
delete leftOvers[i].parentId
possibleParent.children ? possibleParent.children.push(leftOvers[i]) : possibleParent.children = [leftOvers[i]]
possibleParent.count = possibleParent.children.length
const addedObj = leftOvers.splice(i, 1)
checkLeftOvers(leftOvers, addedObj[0])
}
}
}
function findParent(possibleParents, possibleChild) {
let found = false
for (let i = 0; i < possibleParents.length; i++) {
if(possibleParents[i].id === possibleChild.parentId) {
found = true
delete possibleChild.parentId
if(possibleParents[i].children) possibleParents[i].children.push(possibleChild)
else possibleParents[i].children = [possibleChild]
possibleParents[i].count = possibleParents[i].children.length
return true
} else if (possibleParents[i].children) found = findParent(possibleParents[i].children, possibleChild)
}
return found;
}
const nested = flat.reduce((initial, value, index, original) => {
if (value.parentId === null) {
if (initial.left.length) checkLeftOvers(initial.left, value)
delete value.parentId
value.root = true;
initial.nested.push(value)
}
else {
let parentFound = findParent(initial.nested, value)
if (parentFound) checkLeftOvers(initial.left, value)
else initial.left.push(value)
}
return index < original.length - 1 ? initial : initial.nested
}, {nested: [], left: []})
console.log(nested)
You could a standard approach for a tree which takes a single loop and stores the relation between child and parent and between parent and child.
For having root properties you need an additional check.
Then take an iterative and recursive approach for getting count.
var data = [{ id: 'a', name: 'Root 1', parentId: null }, { id: 'b', name: 'Root 2', parentId: null }, { id: 'c', name: 'Root 3', parentId: null }, { id: 'a1', name: 'Item 1', parentId: 'a' }, { id: 'a2', name: 'Item 1', parentId: 'a' }, { id: 'b1', name: 'Item 1', parentId: 'b' }, { id: 'b2', name: 'Item 2', parentId: 'b' }, { id: 'b3', name: 'Item 3', parentId: 'b' }, { id: 'c1', name: 'Item 1', parentId: 'c' }, { id: 'c2', name: 'Item 2', parentId: 'c' }, { id: 'b2-1', name: 'Item 2-1', parentId: 'b2' }, { id: 'b2-2', name: 'Item 2-2', parentId: 'b2' },],
tree = function (data, root) {
function setCount(object) {
return object.children
? (object.count = object.children.reduce((s, o) => s + 1 + setCount(o), 0))
: 0;
}
var t = {};
data.forEach(o => {
Object.assign(t[o.id] = t[o.id] || {}, o);
t[o.parentId] = t[o.parentId] || {};
t[o.parentId].children = t[o.parentId].children || [];
t[o.parentId].children.push(t[o.id]);
if (o.parentId === root) t[o.id].root = true; // extra
});
setCount(t[root]); // extra
return t[root].children;
}(data, null);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Assuming that the flat items array is always sorted like in your case (parents nodes are sorted before children nodes). The code below should do the work.
First, I build the tree without the count properties using reduce on the array to build a map to keeping a track of every node and linking parents to children:
type NestedItemMap = { [nodeId: string]: NestedItem };
let nestedItemMap: NestedItemMap = flat
.reduce((nestedItemMap: NestedItemMap, item: Item): NestedItemMap => {
// Create the nested item
nestedItemMap[item.id] = {
id: item.id,
name: item.name
}
if(item.parentId == null){
// No parent id, it's a root node
nestedItemMap[item.id].root = true;
}
else{
// Child node
let parentItem: NestedItem = nestedItemMap[item.parentId];
if(parentItem.children == undefined){
// First child, create the children array
parentItem.children = [];
parentItem.count = 0;
}
// Add the child node in it's parent children
parentItem.children.push(
nestedItemMap[item.id]
);
parentItem.count++;
}
return nestedItemMap;
}, {});
The fact that the parents node always come first when reducing the array ensures that the parent node is available in the nestedItemMap when building the children.
Here we have the trees, but without the count properties:
let roots: NestedItem[] = Object.keys(nestedItemMap)
.map((key: string): NestedItem => nestedItemMap[key])
.filter((item: NestedItem): boolean => item.root);
To have the count properties filled, I would personally prefer performing a post-order depth-first search on the trees. But in your case, thanks to the node id namings (sorted, the parents nodes ids come first). You can compute them using:
let roots: NestedItem[] = Object.keys(nestedItemMap)
.map((key: string): NestedItem => nestedItemMap[key])
.reverse()
.map((item: NestedItem): NestedItem => {
if(item.children != undefined){
item.count = item.children
.map((child: NestedItem): number => {
return 1 + (child.count != undefined ? child.count : 0);
})
.reduce((a, b) => a + b, 0);
}
return item;
})
.filter((item: NestedItem): boolean => item.root)
.reverse();
I just reverse the array to get all children first (like in a post-order DFS), and compute the count value.
The last reverse is here just to be sorted like in your question :).
maybe this can help you, input is flat obj
nestData = (data, parentId = '') => {
return data.reduce((result, fromData) => {
const obj = Object.assign({}, fromData);
if (parentId === fromData.parent_id) {
const children = this.nestData(data, fromData.id);
if (children.length) {
obj.children = children;
} else {
obj.userData = [];
}
result.push(obj);
}
return result;
}, []);
};
If you have this much information in advance, you can build the tree backwards a lot easier. Since you know the shape of the input so well and their relationships are clearly defined you can easily separate this into multiple arrays and build this from the bottom up:
function buildTree(arr: Item[]): NestedItem[] {
/* first split the input into separate arrays based on their nested level */
const roots = arr.filter(r => /^\w{1}$/.test(r.id));
const levelOne = arr.filter(r => /^\w{1}\d{1}$/.test(r.id));
const levelTwo = arr.filter(r => /^\w{1}\d{1}-\d{1}$/.test(r.id));
/* then create the bottom most level based on their relationship to their parent*/
const nested = levelOne.map(item => {
const children = levelTwo.filter(c => c.parentId === item.id);
if (children) {
return {
...item,
count: children.length,
children
};
} else return item;
});
/* and finally do the same with the root items and return the result */
return roots.map(item => {
const children = nested.filter(c => c.parentId === item.id);
if (children) {
return {
...item,
count: children.length,
children,
root: true
};
} else return { ...item, root: true };
});
}
This might not be the most performant solution, and it would need some tweaking depending on the expected shape of the input, but it is a clean and readable solution.
Another approach might look like this:
const countKids = (nodes) =>
nodes.length + nodes.map(({children = []}) => countKids(children)).reduce((a, b) => a + b, 0)
const makeForest = (id, xs) =>
xs .filter (({parentId}) => parentId == id)
.map (({id, parentId, ...rest}) => {
const kids = makeForest (id, xs)
return {id, ...rest, ...(kids .length ? {count: countKids (kids), children: kids} : {})}
})
const nest = (flat) =>
makeForest (null, flat)
.map ((node) => ({...node, root: true}))
const flat = [{id: "a", name: "Root 1", parentId: null}, {id: "b", name: "Root 2", parentId: null}, {id: "c", name: "Root 3", parentId: null}, {id: "a1", name: "Item 1", parentId: "a"}, {id: "a2", name: "Item 1", parentId: "a"}, {id: "b1", name: "Item 1", parentId: "b"}, {id: "b2", name: "Item 2", parentId: "b"}, {id: "b2-1", name: "Item 2-1", parentId: "b2"}, {id: "b2-2", name: "Item 2-2", parentId: "b2"}, {id: "b3", name: "Item 3", parentId: "b"}, {id: "c1", name: "Item 1", parentId: "c"}, {id: "c2", name: "Item 2", parentId: "c"}]
console .log (nest (flat))
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function (makeForest) finds all the children whose ids match the target (initially null) and then recursively does the same with those children's ids.
The only complexity here is in not including count or children if the children for a node is empty. If including them is not a problem, then this can be simplified.
this.treeData = this.buildTreeData(
flat.filter(f => !f.parentId), flat
);
private buildTreeData(datagroup: Item[], flat: Item[]): any[] {
return datagroup.map((data) => {
const items = this.buildTreeData(
flat.filter((f) => f.parentId === data.id), flat
);
return {
...data,
root: !data.parentId,
count: items?.length || null
children: items,
};
});
}
Hi i tried the accepted answer by Cody and ran into some problems when data wasn't sorted and for nested data with level>2
in this sandbox:
https://codesandbox.io/s/runtime-dew-g48sk?file=/src/index.js:1875-1890
i just changed the order a bit (id=3 was moved to the end of the list), see how in the console we now get that c has only 1 child
I had another problem where parents couldn't be found, because in findParent function the found var was reseted to false if the function was called recursivly with a first argument being an array longer than 1 (e.g. finding a parent for id=21 in:
{id: 1,parentId: null, children: [
{
id: 10,
parentId: 1,
children: []
},
{
id: 11,
parentId: 1,
children: [{
id: 21...
}]
}
]}
would fail
anyway i think the flow itself was good just needed some minor fixes and renames, so here is what's worked for me, I removed some properties that I didn't use (like counter) and added some of my own (like expanded) but it obviously shouldn't matter at all, also im using TS (but i changed all my types to any):
class NestService {
public nestSearchResultsToTree(flatItemsPath: any[]) {
const nested = flatItemsPath.reduce(
(
initial: { nested: any[]; left: any[] },
value: any,
index: number,
original: any
) => {
if (value.parentId === null) {
if (initial.left.length) this.checkLeftOvers(initial.left, value);
initial.nested.push(value);
} else {
const parentFound = this.findParent(initial.nested, value);
if (parentFound) this.checkLeftOvers(initial.left, value);
else initial.left.push(value);
}
return index < original.length - 1 ? initial : initial.nested;
},
{ nested: [], left: [] }
);
return nested;
}
private checkLeftOvers(leftOvers: any[], possibleParent: any) {
for (let i = 0; i < leftOvers.length; i++) {
const possibleChild = leftOvers[i];
if (possibleChild.id === possibleParent.id) continue;
if (possibleChild.parentId === possibleParent.id) {
possibleParent.children
? possibleParent.children.push(possibleChild)
: (possibleParent.children = [possibleChild]);
possibleParent.expanded = true;
possibleParent.isFetched = true;
this.checkLeftOvers(leftOvers, possibleChild);
}
}
}
private findParent(
possibleParents: any,
child: any,
isAlreadyFound?: boolean
): boolean {
if (isAlreadyFound) return true;
let found = false;
for (let i = 0; i < possibleParents.length; i++) {
const possibleParent = possibleParents[i];
if (possibleParent.id === child.parentId) {
possibleParent.expanded = true;
possibleParent.isFetched = true;
found = true;
if (possibleParent.children) possibleParent.children.push(child);
else possibleParent.children = [child];
return true;
} else if (possibleParent.children)
found = this.findParent(possibleParent.children, child, found);
}
return found;
}
}