I´m doing some beginner practise on React.js and created a tree view component using material-ui. Now I want to implement a search bar to search for the entered keyword in the tree view.
Here is my sample data:
[
{
id: 1,
name: "Item 1",
children: [
{
id: 2,
name: "Subitem 1",
children: [
{
id: 3,
name: "Misc 1",
children: [
{
id: 4,
name: "Misc 2"
}
]
}
]
},
{
id: 5,
name: "Subitem 2",
children: [
{
id: 6,
name: "Misc 3",
}
]
}
]
},
{
id: 7,
name: "Item 2",
children: [
{
id: 8,
name: "Subitem 1",
children: [
{
id: 9,
name: "Misc 1"
}
]
},
{
id: 10,
name: "Subitem 2",
children: [
{
id: 11,
name: "Misc 4"
},
{
id: 12,
name: "Misc 5"
},
{
id: 13,
name: "Misc 6",
children: [
{
id: 14,
name: "Misc 7"
}
]
}
]
}
]
}
]
The rendering part is working as expected.
const getTreeItemsFromData = treeItems => {
return treeItems.map(treeItemData => {
let children = undefined;
if (treeItemData.children && treeItemData.children.length > 0) {
children = getTreeItemsFromData(treeItemData.children);
}
return(
<TreeItem
key={treeItemData.id}
nodeId={treeItemData.id}
label={treeItemData.name}
children={children}/>
);
});
};
const DataTreeView = ({ treeItems }) => {
return(
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
>
{getTreeItemsFromData(treeItems)}
</TreeView>
);
};
class App extends Component {
render() {
return (
<div className="App">
<DataTreeView treeItems={searchedNodes} />
</div>
);
}
}
Now I´m struggeling to implement the search functionality. I want to use the material-search-bar (https://openbase.io/js/material-ui-search-bar)
This solution assumes that you use unique id for each item in the tree.
It uses Depth first search algorithm.
Before trying please fix your sample data non-unique IDs. I didn't notice them at first and wasted time debugging for no bug.
function dfs(node, term, foundIDS) {
// Implement your search functionality
let isMatching = node.name && node.name.indexOf(term) > -1;
if (Array.isArray(node.children)) {
node.children.forEach((child) => {
const hasMatchingChild = dfs(child, term, foundIDS);
isMatching = isMatching || hasMatchingChild;
});
}
// We will add any item if it matches our search term or if it has a children that matches our term
if (isMatching && node.id) {
foundIDS.push(node.id);
}
return isMatching;
}
function filter(data, matchedIDS) {
return data
.filter((item) => matchedIDS.indexOf(item.id) > -1)
.map((item) => ({
...item,
children: item.children ? filter(item.children, matchedIDS) : [],
}));
}
function search(term) {
// We wrap data in an object to match the node shape
const dataNode = {
children: data,
};
const matchedIDS = [];
// find all items IDs that matches our search (or their children does)
dfs(dataNode, term, matchedIDS);
// filter the original data so that only matching items (and their fathers if they have) are returned
return filter(data, matchedIDS);
}
Related
Im trying to create a React functional component that will populate a select option tags from nested data but having trouble getting it to work.
Here's the working copy of the code at codesandbox: sample project
My data looks like this:
{
channels: [
{
id: "1878",
name: "Audio/Video",
depth: 0,
children: [
{
id: "1885",
name: "Comedy",
depth: 1,
children: []
},
{
id: "1886",
name: "Opera",
depth: 1,
children: []
},
{
id: "1894",
name: "Lifestyle",
depth: 1,
children: [
{
id: "1895",
name: "Fashion",
depth: 2
},
{
id: "1896",
name: "Fitness",
depth: 2
}
]
}
]
},
...
]
}
I created a function that console log the result as I expected. But having trouble translating this to a React functional component.
Here's the function that prints to console (output is what I wanted but as option tabs):
DeepIteratorTree(json);
function DeepIteratorTree(target) {
const channels = target.channels;
// console.log(channels)
return channels.map((el) => {
if (el.depth === 0) {
console.log(el.name);
if (el.children.length !== 0) {
el.children.map((ch) => {
console.log(
`${" ".repeat(Number.parseInt(ch.depth, 10))}${" "}${ch.name}`
);
if (ch.children.length !== 0) {
ch.children.map((chch) => {
console.log(
`${" ".repeat(Number.parseInt(chch.depth, 10))}${" "}${
chch.name
}`
);
return null;
});
}
return null;
});
}
}
return null;
});
}
console out put:
Audio/Video
Comedy
Opera
Lifestyle
Fashion
Fitness
Reader
News
Publishing
Books
Magazines
Gallery
Arts
Shows
Following function works to populate the first level option tags but but not the next level. I have also tried using nested ternary statements but unable to make it work.
function DeepIteratorTree2(target) {
const channels = target.json.channels;
return channels.map((el) => {
if (el.depth === 0) {
return <option key={el.name}>{el.name}</option>;
}
if (el.children.length !== 0) {
return el.children.map((ch) => {
return <option key={ch.name}>{ch.name}</option>;
});
}
return null;
});
// return null
}
Thank you in advance for your time.
Instead of trying to deep iterate and map DOM Nodes yourself in React, I think you can benefit from flattening your json.channels array first and then just simply having a single map over that flattened channels array to render select options like so :-
import "./styles.css";
export default function App() {
const channels = flatten(json.channels);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<select>
<ChannelsSelector channels={channels} />
</select>
</div>
);
}
const json = {
channels: [
{
id: "1878",
name: "Audio/Video",
depth: 0,
children: [
{
id: "1885",
name: "Comedy",
depth: 1,
children: []
},
{
id: "1886",
name: "Opera",
depth: 1,
children: []
},
{
id: "1894",
name: "Lifestyle",
depth: 1,
children: [
{
id: "1895",
name: "Fashion",
depth: 2
},
{
id: "1896",
name: "Fitness",
depth: 2
}
]
}
]
},
{
id: "1879",
name: "Reader",
depth: 0,
children: [
{
id: "1902",
name: "News",
depth: 1,
children: []
},
{
id: "1903",
name: "Publishing",
depth: 1,
children: [
{
id: "1904",
name: "Books",
depth: 2
},
{
id: "1905",
name: "Magazines",
depth: 2
}
]
}
]
},
{
id: "1880",
name: "Gallery",
depth: 0,
children: [
{
id: "1908",
name: "Arts",
depth: 1,
children: []
}
]
},
{
id: "1884",
name: "Shows",
depth: 0,
children: []
}
]
};
function flatten(channels) {
const output = [];
function process(channels) {
for (let index = 0; index < channels.length; index++) {
const channel = channels[index];
output.push(channel.name);
if (channel.children && channel.children.length > 0) {
process(channel.children);
}
}
}
process(channels);
return output;
}
function ChannelsSelector({ channels }) {
return channels.map((channel) => {
return <option key={channel}>{channel}</option>;
});
}
Here is the forked version :-
Note :- There could be a better declarative implementation of flatten function. The above has an imperative one. Following is a declarative one :-
function flatten(channels) {
return channels.reduce((output,channel)=>{
output.push(channel.name);
if(channel.children?.length>0)
{
let children = flatten(channel.children);
output = output.concat(children)
// or output.push(...children)
}
return output;
},[])
}
You are checking
el.depth === 0
Instead of
el.children.length === 0
In this part:
return channels.map((el) => {
if (el.depth === 0) {
return <option key={el.name}>{el.name}</option>;
}
if (el.children.length !== 0) {
return el.children.map((ch) => {
return <option key={ch.name}>{ch.name}</option>;
});
}
return null;
});
Working example:
Working Example
Hope that is what your are trying to achieve...
I'm trying to filter a nested structure, based on a search string.
If the search string is matched in an item, then I want to keep that item in the structure, along with its parents.
If the search string is not found, and the item has no children, it can be discounted.
I've got some code working which uses a recursive array filter to check the children of each item:
const data = {
id: '0.1',
children: [
{
children: [],
id: '1.1'
},
{
id: '1.2',
children: [
{
children: [],
id: '2.1'
},
{
id: '2.2',
children: [
{
id: '3.1',
children: []
},
{
id: '3.2',
children: []
},
{
id: '3.3',
children: []
}
]
},
{
children: [],
id: '2.3'
}
]
}
]
};
const searchString = '3.3';
const filterChildren = (item) => {
if (item.children.length) {
item.children = item.children.filter(filterChildren);
return item.children.length;
}
return item.id.includes(searchString);
};
data.children = data.children.filter(filterChildren);
console.log(data);
/*This outputs:
{
"id": "0.1",
"children": [
{
"id": "1.2",
"children": [
{
"id": "2.2",
"children": [
{
"id": "3.3",
"children": []
}
]
}
]
}
]
}*/
I'm concerned that if my data structure becomes massive, this won't be very efficient.
Can this be achieved in a 'nicer' way, that limits the amount of looping going on? I'm thinking probably using a reducer/transducer or something similarly exciting :)
A nonmutating version with a search for a child.
function find(array, id) {
var child,
result = array.find(o => o.id === id || (child = find(o.children, id)));
return child
? Object.assign({}, result, { children: [child] })
: result;
}
const
data = { id: '0.1', children: [{ children: [], id: '1.1' }, { id: '1.2', children: [{ children: [], id: '2.1' }, { id: '2.2', children: [{ id: '3.1', children: [] }, { id: '3.2', children: [] }, { id: '3.3', children: [] }] }, { children: [], id: '2.3' }] }] },
searchString = '3.3',
result = find([data], searchString);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have a nested array of objects like this:
let data = [
{
id: 1,
title: "Abc",
children: [
{
id: 2,
title: "Type 2",
children: [
{
id: 23,
title: "Number 3",
children:[] /* This key needs to be deleted */
}
]
},
]
},
{
id: 167,
title: "Cde",
children:[] /* This key needs to be deleted */
}
]
All I want is to recursively find leaves with no children (currently an empty array) and remove the children property from them.
Here's my code:
normalizeData(data, arr = []) {
return data.map((x) => {
if (Array.isArray(x))
return this.normalizeData(x, arr)
return {
...x,
title: x.name,
children: x.children.length ? [...x.children] : null
}
})
}
You need to use recursion for that:
let data = [{
id: 1,
title: "Abc",
children: [{
id: 2,
title: "Type 2",
children: [{
id: 23,
title: "Number 3",
children: [] /* This key needs to be deleted */
}]
}]
},
{
id: 167,
title: "Cde",
children: [] /* This key needs to be deleted */
}
]
function traverse(obj) {
for (const k in obj) {
if (typeof obj[k] == 'object' && obj[k] !== null) {
if (k === 'children' && !obj[k].length) {
delete obj[k]
} else {
traverse(obj[k])
}
}
}
}
traverse(data)
console.log(data)
Nik's answer is fine (though I don't see the point of accessing the children key like that), but here's a shorter alternative if it can help:
let data = [
{id: 1, title: "Abc", children: [
{id: 2, title: "Type 2", children: [
{id: 23, title: "Number 3", children: []}
]}
]},
{id: 167, title: "Cde", children: []}
];
data.forEach(deleteEmptyChildren = o =>
o.children.length ? o.children.forEach(deleteEmptyChildren) : delete o.children);
console.log(data);
If children is not always there, you can change the main part of the code to:
data.forEach(deleteEmptyChildren = o =>
o.children && o.children.length
? o.children.forEach(deleteEmptyChildren)
: delete o.children);
Simple recursion with forEach is all that is needed.
let data = [{
id: 1,
title: "Abc",
children: [{
id: 2,
title: "Type 2",
children: [{
id: 23,
title: "Number 3",
children: [] /* This key needs to be deleted */
}]
}, ]
},
{
id: 167,
title: "Cde",
children: [] /* This key needs to be deleted */
}
]
const cleanUp = data =>
data.forEach(n =>
n.children.length
? cleanUp(n.children)
: (delete n.children))
cleanUp(data)
console.log(data)
This assumes children is there. If it could be missing than just needs a minor change to the check so it does not error out on the length check. n.children && n.children.length
You can do it like this using recursion.
So here the basic idea is in removeEmptyChild function we check if the children length is non zero or not. so if it is we loop through each element in children array and pass them function again as parameter, if the children length is zero we delete the children key.
let data=[{id:1,title:"Abc",children:[{id:2,title:"Type2",children:[{id:23,title:"Number3",children:[]}]},]},{id:167,title:"Cde",children:[]},{id:1}]
function removeEmptyChild(input){
if( input.children && input.children.length ){
input.children.forEach(e => removeEmptyChild(e) )
} else {
delete input.children
}
return input
}
data.forEach(e=> removeEmptyChild(e))
console.log(data)
I am looking for a way to be able to search in an array, with nested arrays, a node with information. It can be seen as a tree
const data = [
{
id: '1-1',
name: "Factory",
children: [
{
id: '1-1-1',
name: "Areas",
children: [
{
id: '1-1-1-1',
name: "Sales",
children: [
{
id: '1-1-1-1-1',
name: "Bill Gates",
children:[...]
},
...
]
},
...
]
},
...
],
},
...
]
If I wanted to find the node with name: Bill Gates
Try this function, but it does not work properly
const getElements = (treeData, text) => {
return treeData.map(node => {
const textMatch = node.name.toLowerCase().includes(text.toLowerCase());
if (textMatch) {
console.log(node);
return node;
} else {
if (node.children) {
return getElements(node.children, text)
}
}
})
}
In deeper data like Bill Gates Node returns the entire TreeArray, but with all the data that does not contain the name Bill Gates as undefined
You probably don't want to use .map here, because you don't want a mutated array, you just want to find a node. Using a for loop gets the expected result:
const data = [{
id: '1-1',
name: "Factory",
children: [
{
id: '1-1-1',
name: "Areas",
children: [
{
id: '1-1-1-1',
name: "Sales",
children: [
{
id: '1-1-1-1-1',
name: "Bill Gates",
children:[]
},
]
},
]
},
]
}];
const getElements = (treeData, text) => {
for (let i=0, node = treeData[i]; node; i++) {
const textMatch = node.name.toLowerCase().includes(text.toLowerCase());
if (textMatch) {
console.log(node);
return node;
} else if (node.children) {
return getElements(node.children, text)
}
}
};
getElements(data, 'Bill Gates');
Having a senior-moment, and struggling to get a recursive method to work correctly in Javascript.
There are similar Q&A's here, though nothing I see that has helped me so far.
That being said, if there is indeed a duplicate, i will remove this question.
Given the following array of objects:
var collection = [
{
id: 1,
name: "Parent 1",
children: [
{ id: 11, name: "Child 1", children: [] },
{ id: 12, name: "Child 2", children: [] }
]
},
{
id: 2,
name: "Parent 2",
children: [
{
id: 20,
name: "Child 1",
children: [
{ id: 21, name: "Grand Child 1", children: [] },
{ id: 22, name: "Grand Child 2", children: [] }
]
}
]
},
{
id: 3,
name: "Parent 3",
children: [
{ id: 31, name: "Child 1", children: [] },
{ id: 32, name: "Child 2", children: [] }
]
},
];
I've gone through a few attempts though my method seems to return early after going through one level only.
My latest attempt is:
Can someone please point me in the right direction.
function findType(col, id) {
for (i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
if (col[i].children.length > 0) {
return findType(col[i].children, id);
}
}
return null;
}
I am trying to find an object where a given id matches, so looking for id 1 should return the whole object with the name Parent 1. If looking for id 31 then the whole object with the id 31 and name Child 1 should be returned.
This would translate into
var t = findType(collection, 1);
or
var t = findType(collection, 31);
Note I would like help with a pure JavaScript solution, and not a plugin or other library. Though they may be more stable, it won't help with the learning curve. Thanks.
You was close, you need a variable to store the temporary result of the nested call of find and if found, then break the loop by returning the found object.
Without, you return on any found children without iterating to the end of the array if not found at the first time.
function findType(col, id) {
var i, temp;
for (i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
if (col[i].children.length > 0) {
temp = findType(col[i].children, id); // store result
if (temp) { // check
return temp; // return result
}
}
}
return null;
}
var collection = [{ id: 1, name: "Parent 1", children: [{ id: 11, name: "Child 1", children: [] }, { id: 12, name: "Child 2", children: [] }] }, { id: 2, name: "Parent 2", children: [{ id: 20, name: "Child 1", children: [{ id: 21, name: "Grand Child 1", children: [] }, { id: 22, name: "Grand Child 2", children: [] }] }] }, { id: 3, name: "Parent 3", children: [{ id: 31, name: "Child 1", children: [] }, { id: 32, name: "Child 2", children: [] }] }];
console.log(findType(collection, 31));
console.log(findType(collection, 1));
.as-console-wrapper { max-height: 100% !important; top: 0; }
const findType = (ar, id) => {
return ar.find(item => {
if (item.id === id) {
return item;
}
return item.children.find(cItem => cItem.id === id)
})
}
I think this suffice your needs
You need to ask for the "found" object
let found = findType(col[i].children, id);
if (found) {
return found;
}
Look at this code snippet
var collection = [{ id: 1, name: "Parent 1", children: [{ id: 11, name: "Child 1", children: [] }, { id: 12, name: "Child 2", children: [] } ] }, { id: 2, name: "Parent 2", children: [{ id: 20, name: "Child 1", children: [{ id: 21, name: "Grand Child 1", children: [] }, { id: 22, name: "Grand Child 2", children: [] } ] }] }, { id: 3, name: "Parent 3", children: [{ id: 31, name: "Child 1", children: [] }, { id: 32, name: "Child 2", children: [] } ] }];
function findType(col, id) {
for (let i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
if (col[i].children.length > 0) {
let found = findType(col[i].children, id);
if (found) {
return found;
}
}
}
return null;
}
var t = findType(collection, 31);
console.log(t);
.as-console-wrapper {
max-height: 100% !important
}
1.
Actually your function findType return the value null for every id parameter at the node
{ id: 11, name: "Child 1", children: [] }
When you hit return, it stops all the recursion.
You have to check the return value from the nested call of findType function.
2.
Your for loop should looke like
for (let i = 0; i < col.length; i++)
instead of
for (i = 0; i < col.length; i++)
Because without the let you share a same variable i in the nested call of the function findType and the value will be changed for the dad calling function.
The function could be :
function findType(col, id) {
for (let i = 0; i < col.length; i++) {
if (col[i].id == id) {
return col[i];
}
var nested = findType(col[i].children, id);
if (nested) return nested;
}
return null;
}