Fill treeview from custom array of objects. Recursion call - javascript

I'm using treeview to display my hierarchical data.
I have following array of objects:
const data = [
{ id: 1, hierarchyid: "/", level: 0, name: "Mhz" },
{ id: 2, hierarchyid: "/2/", level: 1, name: "SMT" },
{ id: 3, hierarchyid: "/3/", level: 1, name: "QC" },
{ id: 4, hierarchyid: "/3/4/", level: 2, name: "Tester" },
{ id: 5, hierarchyid: "/3/5/", level: 2, name: "Operator" },
];
I need to fill my treeview with this data.
What I did so far:
getTreeItems(node, nodes) {
const filtered = nodes.filter(
(n) => n.hierarchyid.includes(node.hierarchyid) && n.level != node.level
);
return (
<TreeItem
key={node.id}
nodeId={node.hierarchyid}
label={node.name}
onClick={() => onClicked(node)}
>
{filtered.length > 0
? filtered.map((node) => this.getTreeItems(node, filtered))
: null}
</TreeItem>
);
}
And rendering:
render() {
// The data comes from Server
const data = [
{ id: 1, hierarchyid: "/", level: 0, name: "Mhz" },
{ id: 2, hierarchyid: "/2/", level: 1, name: "SMT" },
{ id: 3, hierarchyid: "/3/", level: 1, name: "QC" },
{ id: 4, hierarchyid: "/3/4/", level: 2, name: "Tester" },
{ id: 5, hierarchyid: "/3/5/", level: 2, name: "Operator" },
];
return (
<TreeView
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: "auto", flexGrow: 1, width: "auto", overflowY: "auto" }}
>
{this.getTreeItems(
{ id: 1, hierarchyid: "/", level: 0, name: "Mhz" },
data
)}
</TreeView>
);
}
}
This giving me view like:
+Mhz
+SMT
+QC
+Tester
+Operator
+Tester // they shouldn't be displayed
+Operator // they have already rendered as child under QC
My problem is can not exclude already rendered nodes.
Update
MUI TreeView supports special JSON data for its nodes. So converting array to JSON also solves the problem. Something like that:
const data = {
id: 1,
hierarchyid: "/",
level: 0,
name: "Mhz",
children: [
{
id: 2,
hierarchyid: "/2/",
level: 1,
name: "SMT"
},
{
id: 3,
hierarchyid: "/3/",
level: 1,
name: "QC",
children: [
{
id: 4,
hierarchyid: "/3/4/",
level: 2,
name: "Tester"
},
{
id: 5,
hierarchyid: "/3/5/",
level: 2,
name: "Operator"
}
]
}
]
};
the array of objects came from Server, so how can I make this Json from array data?

If we write a quick function to test whether one hierarchy id is the direct descendant of another, then we can use it to write a simple recursive version:
const isChild = (prefix) => ({hierarchyid}) =>
hierarchyid .startsWith (prefix)
&& /^[^\/]*\/$/ .test (hierarchyid .slice (prefix .length))
const nest = (xs, prefix = '') =>
xs .filter (isChild (prefix)) .map ((x, _, __, children = nest (xs, x .hierarchyid)) => ({
...x,
... (children .length ? {children} : {})
}))
const data = [{id: 1, hierarchyid: "/", level: 0, name: "Mhz"}, {id: 2, hierarchyid: "/2/", level: 1, name: "SMT"}, {id: 3, hierarchyid: "/3/", level: 1, name: "QC"}, {id: 4, hierarchyid: "/3/4/", level: 2, name: "Tester"}, {id: 5, hierarchyid: "/3/5/", level: 2, name: "Operator"}]
console .log (nest (data))
.as-console-wrapper {max-height: 100% !important; top: 0}
isChild checks whether the hierarchy id starts with our prefix and if the remainder has only one '/', at the very end.
nest is not as efficient as it might be, as it scans the whole array for each node. But I wouldn't worry about it until I had tens of thousands of entries.
If you don't mind having some empty children arrays on your leaves, it's simpler still:
const nest = (xs, prefix = '') =>
xs .filter (isChild (prefix)) .map ((x) => ({
...x,
children: nest (xs, x .hierarchyid)
}))

SO I've looked at the documentation and they've clearly mentioned a way to design your data object in such a way so that the hierarchy can be shown without multiple nodes repeating.
Try going through this once and change your render functions according to the documentation, you can probably get your desired result.
const data: RenderTree = {
id: "root",
name: "Mhz",
children: [
{
id: "1",
name: "SMT"
},
{
id: "3",
name: "QZ",
children: [
{
id: "4",
name: "Tester"
},
{
id: "4",
name: "Operator"
}
]
}
]
};

Related

How to expand an object in an array in JavaScirpt

I'm trying to expand array in JavaScript.
The object ↓
const tests = [
{
id: 1,
name: 'taro',
designs: [
{
designId: 1,
designName: "design1"
},
{
designId: 2,
designName: "design2"
}
]
},
{
id: 2,
name: 'John',
designs: [
{
designId: 3,
designName: "design3"
},
{
designId: 4,
designName: "design4"
}
]
},
{
id: 3,
name: 'Lisa',
designs: []
},
];
[
{ id: 1, name: 'taro', designId: 1, designName: 'design1' },
{ id: 1, name: 'taro', designId: 2, designName: 'design2' },
{ id: 2, name: 'John', designId: 3, designName: 'design3' },
{ id: 2, name: 'John', designId: 4, designName: 'design4' },
{ id: 3, name: 'Lisa', designId: null, designName: null },
]
It is easy to do this using double for, but I want to use it with higher-order functions.
The code I wrote
for (let i = 0; i < tests.length; i++) {
for (let j = 0; j < tests[i].designs.length; j++) {
const id = tests[i].id
const name = tests[i].name
result.push({
id,
name,
designId: tests[i].designs[j].designId,
designName: tests[i].designs[j].designName
})
}
}
In addition, it would be appreciated if you could additionally explain the difference in performance between double for and higher-order functions.
You can use .flatMap() on your tests array with an inner .map() on each designs array. The inner map on the designs array will take the properties from the currently iterated design object and merge it with the properties from the parent object. The outer .flatMap() can then be used to concatenate all returned maps into the one array:
const tests = [ { id: 1, name: 'taro', designs: [ { designId: 1, designName: "design1" }, { designId: 2, designName: "design2" } ] }, { id: 2, name: 'John', designs: [ { designId: 3, designName: "design3" }, { designId: 4, designName: "design4" } ] }, ];
const res = tests.flatMap(({designs, ...rest}) => designs.map(design => ({
...rest,
...design
})));
console.log(res);
Edit:
If you need null values to appear for your design objects if your designs array is empty, you can add the keys explicitly to a new object that you can return when the designs array is empty:
const tests = [ { id: 1, name: 'taro', designs: [] }, { id: 2, name: 'John', designs: [] }, ];
const res = tests.flatMap(({designs, ...rest}) =>
designs.length
? designs.map(design => ({
...rest,
...design
}))
: {...rest, designId: null, designName: null}
);
console.log(res);
You can use an Array.reduce function with Array.map to generate the array:
const results = tests.reduce((acc, { designs, ...rest }) => [
...acc,
...designs.map(e => ({ ...rest, ...e }))
], []);
const tests = [
{
id: 1,
name: 'taro',
designs: [
{
designId: 1,
designName: "design1"
},
{
designId: 2,
designName: "design2"
}
]
},
{
id: 2,
name: 'John',
designs: [
{
designId: 3,
designName: "design3"
},
{
designId: 4,
designName: "design4"
}
]
},
];
const results = tests.reduce((acc, { designs, ...rest }) => [
...acc,
...designs.map(e => ({ ...rest, ...e }))
], []);
console.log(results);
You can use the higher-order function Array.prototype.reduce() with Array.prototype.map()
const newArr = tests.reduce((prev, {designs, ...current}) => [
...prev, ...designs.map(design => ({...design,...current}));
]
, []);
The performance in your approach and this higher-order approach is the same because Array.prototype.reduce runs through the whole array and just facilitates the initialValue approach for us.

How to sort by a specific value with localeCompare?

I want to sort my current array by having live at the front, instead of sorting it alphabetically so that the items with live will be at the front, followed by schedule and lastly end, but the way I'm doing it now with localeCompare, it will only sort alphabetically.
So how can I do so to sort the array in a specific way that I want?
Array Before Sorting Example
const [bulletins] = useState([
{
id: 1,
liveStatus: 'live',
},
{
id: 2,
liveStatus: 'live',
},
{
id: 3,
liveStatus: 'end',
},
{
id: 4,
liveStatus: 'schedule',
},
{
id: 5,
liveStatus: 'end',
}
]);
Sorted Array Example
const [bulletins] = useState([
{
id: 3,
liveStatus: 'end',
},
{
id: 5,
liveStatus: 'end',
},
{
id: 2,
liveStatus: 'live',
},
{
id: 1,
liveStatus: 'live',
},
{
id: 4,
liveStatus: 'schedule',
}
]);
const displayBulletins = bulletins
.sort((a,b) => {
return a.liveStatus.localeCompare(b.liveStatus)
})
.map((bulletin) => {
return (
<Fragment key={bulletin.id}>
<BulletinList
bulletin={bulletin}
/>
</Fragment>
);
})
You can use indexOf to check the first letter in a list of sorted letters ('lse' in this case):
const bulletins = [{id: 1,liveStatus: 'live',},{id: 2,liveStatus: 'live',},{id: 3,liveStatus: 'end',},{id: 4,liveStatus: 'schedule',},{id: 5,liveStatus: 'end',}];
bulletins.sort((a, b) => 'lse'.indexOf(a.liveStatus[0]) - 'lse'.indexOf(b.liveStatus[0]));
console.log(bulletins);
Then no need to sort the elements, all you can use is reduce and arrange in the way you like
const arr = [
{
id: 1,
liveStatus: "live",
},
{
id: 2,
liveStatus: "live",
},
{
id: 3,
liveStatus: "end",
},
{
id: 4,
liveStatus: "schedule",
},
{
id: 5,
liveStatus: "end",
},
];
const dict = { live: 0, schedule: 1, end: 2 };
const resultObj = Array.from({ length: Object.keys(dict).length + 1 }, () => []);
const result = arr
.reduce((acc, curr) => {
acc[dict[curr.liveStatus] ?? resultObj.length - 1].push(curr);
return acc;
}, resultObj)
.flat();
console.log(result);
/* This is not a part of answer. It is just to give the output fill height. So IGNORE IT */
.as-console-wrapper {
max-height: 100% !important;
top: 0;
}

Treeview in javascript

I need to make something similar to treeview. It doesn't need collapsing it just needs to show some heirachy, but in a table view.
Flat data comes in from a database. I unflattened it and made a tree, but now that it's a tree, I wanted to turn it back into an array, so I can easily iterate using a for loop.
After looking at the source code of other treeviews my method was going to be like this:
From flat data from a db, unflatten:
[
{ id: 1, name: 'node1', parentId: 0 },
{ id: 2, name: 'node2', parentId: 1 },
{ id: 4, name: 'node4', parentId: 2 },
{ id: 5, name: 'node5', parentId: 2 },
{ id: 6, name: 'node6', parentId: 3 },
{ id: 3, name: 'node3', parentId: 1 },
]
The tree is now ordered and has a hierarchy (levels for indentation). Traverse the tree. I add level and children.
[
id: 1,
name: 'node1',
level: 0,
children: [
{
id: 2,
name: 'node2',
parentId: 1,
level: 1
children: [
{
id: 4,
name: 'node4',
parentId: 2,
level: 2,
children: []
},
{
id: 5,
name: 'node5',
parentId: 2,
children: []
},
]
},
{
id: 3,
name: 'node1',
parentId: 1,
children: [
{
id: 6,
name: 'node6',
parentId: 3,
children: []
},
]
},
]
]
Compress it back into an array form with order, level.
[
{ id: 1, name: 'node1', level: 0, parentId: 0, children: [...] },
{ id: 2, name: 'node2', level: 1, parentId: 1, children: [...] },
{ id: 4, name: 'node4', level: 2, parentId: 2, children: [...] },
{ id: 5, name: 'node5', level: 2, parentId: 2, children: [...] },
{ id: 3, name: 'node3', level: 1, parentId: 1, children: [...] },
{ id: 6, name: 'node6', level: 2, parentId: 3, children: [...] },
]
Of which I can easily create a table from.
I've gotten close with the following code:
var data = [
{ id: 1, name: 'node1', parentId: 0 },
{ id: 2, name: 'node2', parentId: 1 },
{ id: 4, name: 'node4', parentId: 2 },
{ id: 5, name: 'node5', parentId: 2 },
{ id: 6, name: 'node6', parentId: 3 },
{ id: 3, name: 'node3', parentId: 1 }
]
function unflatten (arr, parentId, level) {
let output = []
for (const obj of arr) {
if (obj.parentId === parentId) {
var children = unflatten(arr, obj.id, level+1)
obj.level = level
if (children.length) {
obj.children = children
}
output.push(obj)
}
}
// console.log(output)
return output
}
function flatten (tree) {
var output = []
for(const node of tree) {
if(node.children !== undefined){
var nodeChildren = flatten(node.children.reverse())
for(const child of nodeChildren){
output.push(child)
}
}
output.push(node)
}
return output
}
var dataCopy = Object.assign([], data)
console.log('data', dataCopy)
var res = unflatten(data, 0, 0)
console.log('tree', res)
var resCopy = Object.assign([], res)
var res2 = flatten(resCopy)
console.log('reflatten', res2)
Fiddle http://jsfiddle.net/ctd09r85/10/
That fiddle is the closest I've gotten, but it's a bit reversed and out of order.
How can I do this, and is this a reasonable way to build the tree view.

Finding a path to target object in nested array of objects in Javascript

I'm currently working through a problem that I'm having some trouble figuring out where I need to find a child node in an array of objects. The target could be one or many levels deep.
The issue is, once I find the object, I also need to push the path I took to get to that object into the resulting data array.
Currently, I have written code that can successfully find the child node:
const buildFullTree = (tree, cat, data = []) => {
let collection = [tree]
while (collection.length) {
let node = collection.shift()
if (node.id === cat.id) {
data.push(node)
}
collection.unshift(...node.children)
}
return data
}
However, this isn't sufficient in terms of getting the path taken to that object.
I'm pretty sure that I need to change this to a recursive depth-first search solution in order to achieve what I'm looking for, but I am not sure how to change the while loop to accomplish this.
If I understand your question correctly, then perhaps you could revise your path search function like so to achieve what you require:
const buildFullTree = (departmentTree, category, data = []) => {
const findPath = (node, category) => {
//If current node matches search node, return tail of path result
if (node.id === category.id) {
return [node]
} else {
//If current node not search node match, examine children. For first
//child that returns an array (path), prepend current node to that
//path result
for (const child of node.children) {
const childPath = findPath(child, category)
if (Array.isArray(childPath)) {
childPath.unshift(child)
return childPath
}
}
}
}
const foundPath = findPath(departmentTree, category)
// If search from root returns a path, prepend root node to path in
// data result
if (Array.isArray(foundPath)) {
data.push(departmentTree)
data.push(...foundPath)
}
return data
}
const departmentTree = {
id: 5,
title: 'department',
level: 1,
children: [{
id: 1,
parentId: 5,
title: 'category',
level: 2,
children: [{
id: 15,
parentId: 1,
title: 'subcategory',
level: 3,
children: []
}, {
id: 18,
parentId: 1,
level: 3,
title: 'subcategory',
children: []
}, {
id: 26,
parentId: 1,
level: 3,
title: 'subcategory',
children: [{
id: 75,
parentId: 26,
level: 4,
title: 'sub-subcategory',
children: []
}, {
id: 78,
parentId: 26,
level: 4,
title: 'sub-subcategory',
children: []
}]
}]
}, {
id: 23823,
title: 'category',
level: 2,
children: []
}, {
id: 9,
parentId: 5,
level: 2,
title: 'category',
children: [{
id: 48414,
parentId: 9,
level: 3,
title: 'subcategory',
children: []
}, {
id: 2414,
parentId: 9,
level: 3,
title: 'subcategory',
children: []
}, {
id: 42414,
parentId: 9,
level: 3,
title: 'subcategory',
children: [{
id: 2323213,
parentId: 42414,
level: 4,
title: 'sub-subcategory',
children: []
}, {
id: 322332,
parentId: 42414,
level: 4,
title: 'sub-subcategory',
children: []
}]
}]
}]
};
console.log('Path to 2323213:',
buildFullTree(departmentTree, {
id: 2323213
}).map(node => node.id).join(' -> '))
console.log('Path to 23823:',
buildFullTree(departmentTree, {
id: 23823
}).map(node => node.id).join(' -> '))
console.log('Path to -1 (non existing node):',
buildFullTree(departmentTree, {
id: -1
}).map(node => node.id).join(' -> '))

How to build a tree from a flat list in FP JS

I'm learning Functional Javascript and encounter into a problem.
I have this flat object:
const data = [
{id: 1, name: "Folder1", parentId: null},
{id: 2, name: "Folder2", parentId: null},
{id: 3, name: "Folder3", parentId: 1},
{id: 4, name: "Folder4", parentId: 2},
{id: 5, name: "Folder5", parentId: 3},
{id: 6, name: "Folder6", parentId: 3}
]
I desire to convert it to this hierarchical object, using only pure functions, no fors, ifs and other "imperative style statements".
Result should be:
[{
id: 1,
name: "Folder1",
parentId: null,
children = [{
id: 3,
name: "Folder3",
parentId: 1,
children = [{
id: 5,
name: "Folder5",
parentId: 3
},
{
id: 6,
name: "Folder6",
parentId: 3
}
]
}]
},
{
id: 2,
name: "Folder2",
parentId: null,
children = [{
id: 4,
name: "Folder4",
parentId: 2
}]
}
]
Any Ideas?
This is a proposal without if, but with Array#reduce and Map. It needs a sorted array.
var data = [{ id: 1, name: "Folder1", parentId: null }, { id: 2, name: "Folder2", parentId: null }, { id: 3, name: "Folder3", parentId: 1 }, { id: 4, name: "Folder4", parentId: 2 }, { id: 5, name: "Folder5", parentId: 3 }, { id: 6, name: "Folder6", parentId: 3 }],
tree = data
.reduce(
(m, a) => (
m
.get(a.parentId)
.push(Object.assign({}, a, { children: m.set(a.id, []).get(a.id) })),
m
),
new Map([[null, []]])
)
.get(null);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or the same as above using ES2015 destructuring assignment. It needs a sorted array and also depends on the input data having only id, name and parentId keys.
var data = [{ id: 1, name: "Folder1", parentId: null }, { id: 2, name: "Folder2", parentId: null }, { id: 3, name: "Folder3", parentId: 1 }, { id: 4, name: "Folder4", parentId: 2 }, { id: 5, name: "Folder5", parentId: 3 }, { id: 6, name: "Folder6", parentId: 3 }],
tree = data
.reduce(
(m, {id, name, parentId}) => (
m
.get(parentId)
.push({id, name, parentId, children: m.set(id, []).get(id) }),
m
),
new Map([[null, []]])
)
.get(null);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Of course this should probably be written as a reusable function ...
var data = [{ id: 1, name: "Folder1", parentId: null }, { id: 2, name: "Folder2", parentId: null }, { id: 3, name: "Folder3", parentId: 1 }, { id: 4, name: "Folder4", parentId: 2 }, { id: 5, name: "Folder5", parentId: 3 }, { id: 6, name: "Folder6", parentId: 3 }];
// pure, reusable function
var buildTree = (data) =>
data.reduce(
(m, {id, name, parentId}) => (
m
.get(parentId)
.push({id, name, parentId, children: m.set(id, []).get(id) }),
m
),
new Map([[null, []]])
)
.get(null);
console.log(buildTree(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Lastly, if the data is arriving in an unsorted order, we could handle sorting with a custom comparator
// unsorted data example
var data = [{ id: 6, name: "Folder6", parentId: 3 }, { id: 2, name: "Folder2", parentId: null }, { id: 3, name: "Folder3", parentId: 1 }, { id: 4, name: "Folder4", parentId: 2 }, { id: 5, name: "Folder5", parentId: 3 }, { id: 1, name: "Folder1", parentId: null }];
// immutable sort
var sort = (f,xs) => [...xs.sort(f)];
// custom tree comparator
var treeComparator = (x,y) =>
x.parentId - y.parentId || x.id - y.id;
// sort data, then reduce
var buildTree = (data) =>
sort(treeComparator, data).reduce(
(m, {id, name, parentId}) => (
m
.get(parentId)
.push({id, name, parentId, children: m.set(id, []).get(id) }),
m
),
new Map([[null, []]])
)
.get(null);
console.log(buildTree(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can do this with recursive function but you need to loop array with reduce and use if statements.
const arr = [
{id: 1, name: "Folder1", parentId: null},
{id: 2, name: "Folder2", parentId: null},
{id: 3, name: "Folder3", parentId: 1},
{id: 4, name: "Folder4", parentId: 2},
{id: 5, name: "Folder5", parentId: 3},
{id: 6, name: "Folder6", parentId: 3}
]
function buildTree(data, pId) {
return data.reduce(function(r, e) {
var e = Object.assign({}, e);
if (e.parentId == pId) {
var children = buildTree(data, e.id)
if (children.length) e.children = children
r.push(e)
}
return r;
}, [])
}
console.log(buildTree(arr, null))
const data = [
{id: 1, name: "Folder1", parentId: null},
{id: 2, name: "Folder2", parentId: null},
{id: 3, name: "Folder3", parentId: 1},
{id: 4, name: "Folder4", parentId: 2},
{id: 5, name: "Folder5", parentId: 3},
{id: 6, name: "Folder6", parentId: 3}
];
function trampoline ( f ) {
while ( f && f instanceof Function ) { f = f ( ); }
return f;
}
function buildTree ( data, copy, top = [] ) {
function recur ( data, copy, top ) {
copy = copy || data.concat ( [] );
let current = copy.shift ( );
current ? doWork ( ) : null;
function doWork ( ) {
top = top.concat ( ( ! current.parentId ? current : [] ) );
current.children = copy.filter ( x => { return current.id === x.parentId } );
}
return ( current ? recur.bind ( null, data, copy, top ) : top );
}
return trampoline ( recur.bind ( null, data, copy, top ) );
}
data.map ( x => { x [ 'children' ] = [ ]; return x; } );
console.log ( buildTree ( data ) );

Categories