Convert array of flat objects to nested objects - javascript

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;
}
}

Related

Get the branch of collection without the sibling elements, searching by property

I have the object with the next structure:
let array = [
{
name: 'Name1',
items: [
{
name: 'Name1.1',
items: [
{ id: '1', name: 'Name1.1.1' },
{ id: '2', name: 'Name1.1.2' },
{ id: '3', name: 'Name1.1.3' },
...
],
},
{
name: 'Name1.2',
items: [
{ id: '4', name: 'Name1.2.1' },
{ id: '5', name: 'Name1.2.2' },
],
},
],
},
{
name: 'Name2',
items: [
{
name: 'Name2.1',
items: [
{ id: '6', name: 'Name2.1.1' },
{ id: '7', name: 'Name2.1.2' },
],
},
],
},
];
I want to get the branch without the sibling elements, searching by id. The desired result is the next structure by id = '4':
let array = [
{
name: 'Name1',
items: [
{
name: 'Name1.2',
items: [
{ id: '4', name: 'Name1.2.1' },
],
},
],
}
];
I could find only the end element of the tree ({ id: '4', name: 'Name1.2.1' }). But I don't understand how to get intermediate structures of the tree.
const test = (data, id) => {
if (!data || !data.length) return null;
for (var j = 0; j < data.length; j++) {
var result = data[j].items
? test(data[j].items, id)
: data[j].id
? data[j].id === id
? data[j]
: undefined
: undefined;
if (result !== undefined) {
return result;
}
}
return undefined;
};
test(array, '4');
You should indeed take a recursive approach, but your function currently can only return an id value (a string) or null or undefined. It never returns an array, yet that is what you expect to get.
When a solution is found as a base case, you need to wrap that solution in an array and plain object, each time you get out of the recursion tree.
Here is a working solution:
function getPath(forest, targetid) {
for (let root of forest) {
if (root.id === targetid) return [root]; // base case
let items = root.items && getPath(root.items, targetid);
if (items) return [{ ...root, items }]; // wrap!
}
}
// Example run:
let array = [{name: 'Name1',items: [{name: 'Name1.1',items: [{ id: '1', name: 'Name1.1.1' },{ id: '2', name: 'Name1.1.2' },{ id: '3', name: 'Name1.1.3' },],},{name: 'Name1.2',items: [{ id: '4', name: 'Name1.2.1' },{ id: '5', name: 'Name1.2.2' },],},],},{name: 'Name2',items: [{name: 'Name2.1',items: [{ id: '6', name: 'Name2.1.1' },{ id: '7', name: 'Name2.1.2' },],},],},];
console.log(getPath(array, '4'));

How to change all occurrences of an object key in an array of objects

I have this sample data:
const data = [
{
id: 1,
title: 'Sports',
menus: [
{
id: 2,
title: 'Basketball',
menus: [
{
id: 3,
title: 'NBA',
},
{
id: 4,
title: 'NCAA',
},
{
id: 5,
title: 'G-League',
},
],
},
],
},
{
id: 100,
title: 'Names',
menus: [],
},
];
I want to change all the menus keys into children, so the result would be:
const result = [
{
id: 1,
title: 'Sports',
children: [
{
id: 2,
title: 'Basketball',
children: [
{
id: 3,
title: 'NBA',
},
{
id: 4,
title: 'NCAA',
},
{
id: 5,
title: 'G-League',
},
],
},
],
},
{
id: 100,
title: 'Names',
children: [],
},
];
I'm trying with this code:
const replacer = { menus: 'children' };
const transform = useCallback(
(obj) => {
if (obj && Object.getPrototypeOf(obj) === Object.prototype) {
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [replacer[k] || k, transform(v)]));
}
return obj;
},
[replacer]
);
but it only changes the keys at the first level. How can I make it work?
You can use a recursive function that makes use of destructuring:
const replaceKey = arr =>
arr.map(({menus, ...o}) =>
menus ? {...o, children: replaceKey(menus)} : o);
const data = [{id: 1,title: 'Sports',menus: [{id: 2,title: 'Basketball',menus: [{id: 3,title: 'NBA',},{id: 4,title: 'NCAA',},{id: 5,title: 'G-League',},],},],},{id: 100,title: 'Names',menus: [],},];
console.log(replaceKey(data));
To provide the old/new key dynamically, use the following variant:
const replaceKey = (arr, source, target) =>
arr.map(({[source]: v, ...o}) =>
v ? {...o, [target]: replaceKey(v, source, target)} : o);
const data = [{id: 1,title: 'Sports',menus: [{id: 2,title: 'Basketball',menus: [{id: 3,title: 'NBA',},{id: 4,title: 'NCAA',},{id: 5,title: 'G-League',},],},],},{id: 100,title: 'Names',menus: [],},];
console.log(replaceKey(data, "menus", "children"));
This code assumes that values for the given key are arrays. If for some reason their values could be something else, then the code needs a bit more extension:
const data = [{id: 1,title: 'Sports',menus: [{id: 2,title: 'Basketball',menus: [{id: 3,title: 'NBA',},{id: 4,title: 'NCAA',},{id: 5,title: 'G-League',},],},],},{id: 100,title: 'Names',menus: 13,},];
const replaceKey = (arr, source, target) =>
Array.isArray(arr) ? arr.map(({[source]: value, ...o}) =>
value !== undefined ? {...o, [target]: replaceKey(value, source, target)} : o
) : arr;
console.log(replaceKey(data, "menus", "children"));
To see the effect of this code, the value for the very last menus key was changed to 13.
If the object is not big:
let data=[{id:1,title:'Sports',menus:[{id:2,title:'Basketball',menus:[{id:3,title:'NBA',},{id:4,title:'NCAA',},{id:5,title:'G-League',},],},],},{id:100,title:'Names',menus:[],},];
data = JSON.parse(JSON.stringify(data).replace(/"menus"\:/g,'"children":'))
console.log(data)
check this package: paix: that's take original source object and desired keys replacement then return a new object with desired keys, ex:
npm i paix
import { paix } from 'paix';
const data = [
{
id: 1,
title: 'Sports',
menus: [
{
id: 2,
title: 'Basketball',
menus: [
{
id: 3,
title: 'NBA',
},
],
},
],
},
{
id: 100,
title: 'Names',
menus: [],
},
];
const keys_swap = {menus: "children"};
const result = data.map(i => paix(i, keys_swap));

Find Duplicate Object in List and Add Parameters

I am trying to find duplicate objects in a list of objects and add new parameters to the duplicate one.
Below snipped code is what I implemented so far. The problem is that it adds desired parameters to every object in the list.
const list = [{
id: 1,
name: 'test1'
},
{
id: 2,
name: 'test2'
},
{
id: 3,
name: 'test3'
},
{
id: 2,
name: 'test2'
}
];
const newList = list.reduce(
(unique, item) => (unique.includes(item) ? unique : [...unique, {
...item,
duplicated: true,
name: `${item.name}_${item.id}`
}]), []
);
console.log(newList);
Since there are two duplicate objects by id, the duplicated one should have duplicated and new name parameters. What part is wrong in my implementation?
By using findIndex method:
const list = [{
id: 1,
name: 'test1'
},
{
id: 2,
name: 'test2'
},
{
id: 3,
name: 'test3'
},
{
id: 2,
name: 'test2'
}
];
const newList = list.reduce(
(unique, item) => (unique.findIndex(x => x.id === item.id) > -1 ? [...unique, {
...item,
duplicated: true,
name: `${item.name}_${item.id}`
}] : [...unique, item]), []);
console.log(newList);
It can be written simply:
const
list = [
{ id: 1, name: 'test1' },
{ id: 2, name: 'test2' },
{ id: 3, name: 'test3' },
{ id: 2, name: 'test2' }
],
uniqueList = list.reduce((arr, { id, name }) =>
arr.concat({
id,
name,
...arr.some(item => id === item.id) && { duplicate: true, name: `${name}_${id}` }
}), []);
console.log(uniqueList);
The problem was that when you called includes you were actually looking for an object whose pointer exists in the array.
In order to find an object which has property are the same as a requested property, you have no choice but to use functions such as some or every that is different from includes - you can send them a callback and not just an object.

Remove matched object from deeply nested array of objects

I have a data tree structure with children:
{ id: 1,
name: "Dog",
parent_id: null,
children: [
{
id: 2,
name: "Food",
parent_id: 1,
children: []
},
{
id: 3,
name: "Water",
parent_id: 1,
children: [
{
id: 4,
name: "Bowl",
parent_id: 3,
children: []
},
{
id: 5,
name: "Oxygen",
parent_id: 3,
children: []
},
{
id: 6,
name: "Hydrogen",
parent_id: 3,
children: []
}
]
}
]
}
This represents a DOM structure that a user could select an item from to delete by clicking the corresponding button in the DOM.
I have a known text title of the selected item for deletion from the DOM set as the variable clickedTitle. I am having trouble finding an algorithm that will allow me to delete the correct object data from the deeply nested tree.
Here is my code:
function askUserForDeleteConfirmation(e) {
const okToDelete = confirm( 'Are you sure you want to delete the item and all of its sub items?' );
if(!okToDelete) {
return;
}
const tree = getTree(); // returns the above data structure
const clickedTitle = getClickedTitle(e); // returns string title of clicked on item from DOM - for example "Dog" or "Bowl"
const updatedTree = removeFromTree(tree, tree, clickedTitle);
return updatedTree;
}
function removeFromTree(curNode, newTree, clickedTitle) {
if(curNode.name === clickedTitle) {
// this correctly finds the matched data item to delete but the next lines don't properly delete it... what to do?
const index = curNode.children.findIndex(child => child.name === clickedTitle);
newTree = curNode.children.slice(index, index + 1);
// TODO - what to do here?
}
for(const node of curNode.children) {
removeFromTree(node, newTree, clickedTitle);
}
return newTree;
}
I have tried to use the info from Removing matched object from array of objects using javascript without success.
If you don't mind modifying the parameter tree in-place, this should do the job. Note that it'll return null if you attempt to remove the root.
const tree = { id: 1, name: "Dog", parent_id: null, children: [ { id: 2, name: "Food", parent_id: 1, children: [] }, { id: 3, name: "Water", parent_id: 1, children: [ { id: 4, name: "Bowl", parent_id: 3, children: [] }, { id: 5, name: "Oxygen", parent_id: 3, children: [] }, { id: 6, name: "Hydrogen", parent_id: 3, children: [] } ] } ] };
const removeFromTree = (root, nameToDelete, parent, idx) => {
if (root.name === nameToDelete) {
if (parent) {
parent.children.splice(idx, 1);
}
else return null;
}
for (const [i, e] of root.children.entries()) {
removeFromTree(e, nameToDelete, root, i);
}
return tree;
};
console.log(removeFromTree(tree, "Oxygen"));
Your current code is very much on the right track. However:
newTree = curNode.children.slice(index, index + 1);
highlights a few issues: we need to manipulate the parent's children array to remove curNode instead of curNode's own children array. I pass parent objects and the child index recursively through the calls, saving the trouble of the linear operation findIndex.
Additionally, slicing from index to index + 1 only extracts one element and doesn't modify curNode.children. It's not obvious how to go about using newArray or returning it through the call stack. splice seems like a more appropriate tool for the task at hand: extracting one element in-place.
Note that this function will delete multiple entries matching nameToDelete.
I like #VictorNascimento's answer, but by applying map then filter, each children list would be iterated twice. Here is an alternative with reduce to avoid that:
function removeFromTree(node, name) {
return node.name == name
? undefined
: {
...node,
children: node.children.reduce(
(children, child) => children.concat(removeFromTree (child, name) || []), [])
}
}
In the case you want a way to remove the items in-place, as #ggorlen proposed, I'd recommend the following solution, that is simpler in my opinion:
function removeFromTree(node, name) {
if (node.name == name) {
node = undefined
} else {
node.children.forEach((child, id) => {
if (!removeFromTree(child, name)) node.children.splice(id, 1)
})
}
return node
}
I've built the algorithm as follows:
function omitNodeWithName(tree, name) {
if (tree.name === name) return undefined;
const children = tree.children.map(child => omitNodeWithName(child, name))
.filter(node => !!node);
return {
...tree,
children
}
}
You can use it to return a new tree without the item:
noHydrogen = omitNodeWithName(tree, "Hydrogen")
If it's ok to use Lodash+Deepdash, then:
let cleaned = _.filterDeep([tree],(item)=>item.name!='Hydrogen',{tree:true});
Here is a Codepen
We use object-scan for many data processing tasks. It's powerful once you wrap your head around it. Here is how you could answer your question
// const objectScan = require('object-scan');
const prune = (name, input) => objectScan(['**[*]'], {
rtn: 'bool',
abort: true,
filterFn: ({ value, parent, property }) => {
if (value.name === name) {
parent.splice(property, 1);
return true;
}
return false;
}
})(input);
const obj = { id: 1, name: 'Dog', parent_id: null, children: [{ id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [{ id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] }] }] };
console.log(prune('Oxygen', obj)); // return true iff pruned
// => true
console.log(obj);
// => { id: 1, name: 'Dog', parent_id: null, children: [ { id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [ { id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] } ] } ] }
.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

Join two objects by key

I stuck on mergin 2 objects into one. Let's say I have 2 arrays of objects:
One is childs:
let childsWithMoreInfo = [{
id: 1,
name: 'somename',
parent: {
id: 2
},
}, {
id: 2,
name: 'some child name',
parent: {
id: 4
}
}];
And the second one is Parents:
let parents = [{
id: 1,
parentName: 'The first',
child: {}
}, {
id: 2,
parentName: 'The second',
child: {}
}, {
id: 3,
parentName: 'The third',
child: {}
}, {
id: 4,
parentName: 'The fourth',
child: {}
}];
And I would to merge these objects like this:
let combined = [
{
id: 1,
parentName: The first,
child: {}
},
{
id: 2,
parentName: The second,
child: {
id: 1,
name: somename,
}
},
{
id: 3,
parentName: The third,
child: {}
},
{
id: 4,
parentName: The fourth,
child: {
id: 2
name: some child name,
}
},
]
];
So basically it should be something like:
let combinedList = parents.child = childsWithMoreInfo where parents.id = childsWithMoreInfo.parent.id . On which method I should take a look? Do you have any ideas how can easily achieve that?
I really know how to use forEach, I wanted to avoid it.
This is what I made:
this.combined = _.map(parents, (parent) => {
parent.child = childs.find(child => child.parent.id === parent.id);
return parent;
});
Thank you for all of your answers.
You could use a Map and iterate then with Array#forEach for every object.
Then have a lookup in the map and assign the values to ch parent object.
var childsWithMoreInfo = [{ id: 1, name: 'somename', parent: { id: 2 } }, { id: 2, name: 'some child name', parent: { id: 4 } }],
parents = [{ id: 1, parentName: 'The first', child: {} }, { id: 2, parentName: 'The second', child: {} }, { id: 3, parentName: 'The third', child: {} }, { id: 4, parentName: 'The fourth', child: {} }],
map = new Map;
parents.forEach(p => map.set(p.id, p));
childsWithMoreInfo.forEach(c => {
var o = map.get(c.parent.id);
o.child = { id: c.id, name: c.name };
});
console.log(parents);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Another solution would be the use of Array#find
var childsWithMoreInfo = [{ id: 1, name: 'somename', parent: { id: 2 } }, { id: 2, name: 'some child name', parent: { id: 4 } }],
parents = [{ id: 1, parentName: 'The first', child: {} }, { id: 2, parentName: 'The second', child: {} }, { id: 3, parentName: 'The third', child: {} }, { id: 4, parentName: 'The fourth', child: {} }];
childsWithMoreInfo.forEach(c => {
var o = parents.find(p => p.id === c.parent.id);
o.child = { id: c.id, name: c.name };
});
console.log(parents);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Following code has some pure functions to do your task. I have formatted/cleaned the input objects also:
'use strict';
let childsWithMoreInfo = [{
id: 1,
name: 'somename',
parent: {
id: 2
},
}, {
id: 2,
name: 'some child name',
parent: {
id: 4
}
}];
let parents = [{
id: 1,
parentName: 'The first',
child: {}
}, {
id: 2,
parentName: 'The second',
child: {}
}, {
id: 3,
parentName: 'The third',
child: {}
}, {
id: 4,
parentName: 'The fourth',
child: {}
}];
function makeObjectFromArray(arr) {
let obj = {};
arr.map(function(item) {
if (obj[item.id] === undefined) {
obj[item.id] = item
}
})
return obj;
}
function toArray(obj) {
return Object.keys(obj).map(function(key) {
return obj[key]
});
}
function sampleParentChildren(parent, children) {
let Parent = {};
if (parent.constructor === Array) {
Parent = makeObjectFromArray(parent);
} else {
Parent = Object.assign({}, parent)
}
children.map(function(child) {
if (Parent[child.parent.id] !== undefined) {
if (Parent[child.parent.id].child === undefined) {
Parent[child.parent.id].child = {};
}
Parent[child.parent.id].child[child.id] = child
}
});
return Parent;
}
let resampledData = sampleParentChildren(parents, childsWithMoreInfo);
console.log(resampledData, toArray(resampledData));
To join the arrays in ES6, you can use the spread operator to join the arrays and just use plain old forEach to deduping. The spread operator is more declarative than Es5's array concatenation methods as mentioned here on MDN
[...parents, ...childsWithMoreInfo]
Here's a working example:
let childsWithMoreInfo = [{
id: 1,
name: 'somename',
parent: {
id: 2
},
}, {
id: 2,
name: 'some child name',
parent: {
id: 4
}
}],
parents = [{
id: 1,
parentName: 'The first',
child: {}
}, {
id: 2,
parentName: 'The second',
child: {}
}, {
id: 3,
parentName: 'The third',
child: {}
}, {
id: 4,
parentName: 'The fourth',
child: {}
},
];
var conjoined = [...parents, ...childsWithMoreInfo];
conjoined.forEach(function(parentConjoinee, parentIndex) {
conjoined.forEach(function(childConjoinee, childIndex) {
if (parentConjoinee.id === childConjoinee.id && parentIndex !== childIndex) {
conjoined.splice(childIndex, 1);
}
});
});
console.log(conjoined);
You can use nested for loops, break
var childsWithMoreInfo = [{
id: 1,
name: "somename",
parent: {
id: 2
}
}, {
id: 2,
name: "some child name",
parent: {
id: 4
}
}];
var parents = [{
id: 1,
parentName: "The first",
child: {}
}, {
id: 2,
parentName: "The second",
child: {}
}, {
id: 3,
parentName: "The third",
child: {}
}, {
id: 4,
parentName: "The fourth",
child: {}
}
];
let combined = [];
for (var i = 0; i < parents.length; i++) {
for (var j = 0; j < childsWithMoreInfo.length; j++) {
if (childsWithMoreInfo[j].parent.id === parents[i].id) {
parents[i].child.id = childsWithMoreInfo[j].id;
parents[i].child.name = childsWithMoreInfo[j].name;
break;
}
}
combined.push(parents[i])
};
console.log(combined);

Categories

Resources