Find element in array and return parent - javascript

I'm having an issue trying to get parent element of found element in array.
This is my array for example:
const arr = [
{
name: 'first level',
selected: true,
subItems: [
{
name: 'second level 1',
selected: false,
subItems: [],
},
{
name: 'second level 2',
selected: true,
subItems: [
{
name: 'third level 1',
selected: false,
subItems: [],
},
{
name: 'third level 2',
selected: false,
subItems: [],
},
{
name: 'third level 3',
selected: false,
subItems: [],
}
]
},
{
name: 'second level 3',
selected: false,
subItems: [
{
name: 'third level 4',
selected: false,
subItems: []
}
]
}
]
}
];
So basically if key selected is true I would like to return it's parent element. Now I don't know how deep can this array be, so I have taken recursive approach to the problem.
const getParent = (items, parentCat = null) => {
if (items && items.length > 0) {
const selectedCat = items.find(item => item.selected === true);
if (selectedCat && selectedCat.subItems.length > 0) {
return getParent(selectedCat.subItems, selectedCat);
}
return parentCat;
}
};
const parent = getParent(arr);
But the code will only work in some cases when there are not subItems for the selected item. I would like to get parent item of deepest selected element.
EDIT: If any element has selected true, also it's parent will have true, also there will always be just one selected element per level.

The problem is that within the recursion, if none of the items are selected, you must return the parent's parent. You can accomplish that by returning null within the deepest recursion, and let the caller handle it as the stack unwinds.
const getParent = (items, parent = null) => {
const selectedItem = items.find(item => item.selected === true);
if (selectedItem) {
// if there was a deeper parent, return that
// otherwise return my own parent
return getParent(selectedItem.subItems, selectedItem) || parent;
} else {
return null;
}
};
const arr = [
{
name: 'first level',
selected: true,
subItems: [
{
name: 'second level 1',
selected: false,
subItems: [],
},
{
name: 'second level 2',
selected: true,
subItems: [
{
name: 'third level 1',
selected: false,
subItems: [],
},
{
name: 'third level 2',
selected: false,
subItems: [],
},
{
name: 'third level 3',
selected: false,
subItems: [],
}
]
},
{
name: 'second level 3',
selected: false,
subItems: [
{
name: 'third level 4',
selected: false,
subItems: []
}
]
}
]
}
];
const getParent = (items, parent = null) => {
const selectedItem = items.find(item => item.selected === true);
if (selectedItem) {
return getParent(selectedItem.subItems, selectedItem) || parent;
} else {
return null;
}
};
console.log(getParent(arr));

Related

Most efficient way to way to find deep nested object using index path and update it

I have an array of tree structures. I need to be able to find the object using the index paths and update the toggle boolean. I am able to update the toggle using the index path, but this logic will need to be updated if more levels of nodes are added to the tree. How can I make it generic, so my code still works even if the treeData is changed?
It'd be nice if I can set all other nodes false if it is in not in the one of the index paths.
treeData = [{
name: 'Infiniti',
toggle: false,
children: [{
name: 'G50',
toggle: false,
children: [{
name: 'Pure AWD',
toggle: false
},
{
name: 'Luxe',
toggle: false
},
],
},
{
name: 'QX50',
toggle: false,
children: [{
name: 'Pure AWD',
toggle: false
},
{
name: 'Luxe',
toggle: false
},
],
},
],
},
{
name: 'BMW',
toggle: false,
children: [{
name: '2 Series',
toggle: false,
children: [{
name: 'Coupé',
toggle: false
},
{
name: 'Gran Coupé',
toggle: false
},
],
},
{
name: '3 Series',
toggle: false,
children: [{
name: 'Sedan',
toggle: false
},
{
name: 'PHEV',
toggle: false
},
],
},
],
},
];
indexPathArray = ["0/0/1", "1/0/0"]
for (const index of indexPathArray) {
const indexArray = index.split('/')
for (let i = 0; i < indexArray.length; i++) {
if (i === 0) {
treeData[indexArray[0]].toggle = true;
}
if (i === 1) {
treeData[indexArray[0]].children[indexArray[1]].toggle = true;
}
if (i === 2) {
treeData[indexArray[0]].children[indexArray[1]].children[indexArray[2]].toggle = true;
}
}
}
console.log(treeData);
You can achieve it with the code like the following:
for (let path of indexPathArray) {
let currentNode = { children: treeData };
for (let i of path.split('/')) {
currentNode = currentNode.children[i];
currentNode.toggle = true;
}
}
The idea here is to have a temporary variable currentNode, which contains the current cursor of the tree traversal.
This will allow you to access next items from the index path, without writing the whole path.
A snippet:
treeData = [{
name: 'Infiniti',
toggle: false,
children: [{
name: 'G50',
toggle: false,
children: [{
name: 'Pure AWD',
toggle: false
},
{
name: 'Luxe',
toggle: false
},
],
},
{
name: 'QX50',
toggle: false,
children: [{
name: 'Pure AWD',
toggle: false
},
{
name: 'Luxe',
toggle: false
},
],
},
],
},
{
name: 'BMW',
toggle: false,
children: [{
name: '2 Series',
toggle: false,
children: [{
name: 'Coupé',
toggle: false
},
{
name: 'Gran Coupé',
toggle: false
},
],
},
{
name: '3 Series',
toggle: false,
children: [{
name: 'Sedan',
toggle: false
},
{
name: 'PHEV',
toggle: false
},
],
},
],
},
];
indexPathArray = ["0/0/1", "1/0/0"]
for (let path of indexPathArray) {
let currentNode = { children: treeData };
for (let i of path.split('/')) {
currentNode = currentNode.children[i];
currentNode.toggle = true;
}
}
console.log(treeData);
With recursion.
Basically, you go in one level at a time and the call the same function as if that inner node is the new root node. Then when you get to the end of your search path, you perform the action you want.
That might look like this:
function togglePath(tree: Tree[], indexPath: string) {
const [first, ...rest] = indexPath.split('/').map(n => parseInt(n))
const children = tree[first]?.children
if (children && rest.length > 0) {
togglePath(children, rest.join('/'))
} else {
tree[first].toggle = true;
}
}
This is a recursive function because it calls itself.
Each each level it check to see if it's there yet with rest.length > 0. If there is only one index path then we found the destination, do the toggle.
If we need to drill in more, then call togglePath again with the children of the node from the first index, and provide the rest of the indices for the next level.
This should work at any level:
console.log(treeData[0].children?.[0].children?.[1].toggle) // false
togglePath(treeData, "0/0/1")
console.log(treeData[0].children?.[0].children?.[1].toggle) // true
console.log(treeData[0].children?.[1].toggle) // false
togglePath(treeData, "0/1")
console.log(treeData[0].children?.[1].toggle) // true
See Typescript Playground
I'd also probably pre-parse your index paths to be an array of integers so you don't have to do that string manipulation at each loop.
Maybe like:
function parseIndexPath(indexPath: string): number[] {
return indexPath.split('/').map(n => parseInt(n))
}
See Playground

ReactJs: Filtering nested objects

How do I filter nested objects in React? Currently my function is only looking for the main title, not the nested ones.
export const treeData = [
{
title: 'Orange',
key: '0-0',
children: [
{
title: 'Anu',
key: '0-0-0',
isLeaf: true,
},
{
title: 'Anurag',
key: '0-0-1',
isLeaf: true,
},
],
},
{
title: 'ABC',
key: '0-1',
children: [
{
title: 'leaf 1-0',
key: '0-1-0',
isLeaf: true,
},
{
title: 'leaf 1-1',
key: '0-1-1',
isLeaf: true,
},
],
},
]
export default {
treeData,
}
and the function is:
const filtered = treeData.filter((data) => {
return data.title.toLowerCase().includes(e.target.value.toLowerCase())
})
So currently it is only searching the Orange and ABC, not the titles from children.
I recommend flattening the title values and then filtering. Note, the end result will contain a mixture of top level "parent" data and children.
const flattened = treeData.reduce((acc, data) => {
acc.push(data, ...data.children)
return acc;
}, []);
const filtered = flattened.filter((data) => {
return data.title.toLowerCase().includes(e.target.value.toLowerCase())
})
If you only need a filtered list of top level data, you will need to do something like the following:
function dataIncludesTitle(data, title) {
return data.title.toLowerCase().includes(title);
}
const searchValue = e.target.value.toLowerCase();
const filtered = treeData.filter((data) => {
return dataIncludesTitle(data, searchValue) || data.children.some(child => dataIncludesTitle(child, searchValue));
})
Something like:
const filtered = treeData.filter((data) => {
return data.title.toLowerCase().includes(e.target.value.toLowerCase()) ||
data.children.filter(x => x.title.toLowerCase().includes(e.target.value.toLowerCase())).length > 0
})
Look into data.children filtering subobject's title that contains e.target.value.toLowerCase().
If you want to get full objects containing child objects with a title similar to the search string, try this:
const treeData = [
{
title: 'Orange',
key: '0-0',
children: [
{
title: 'Anu',
key: '0-0-0',
isLeaf: true,
},
{
title: 'Anurag',
key: '0-0-1',
isLeaf: true,
},
],
},
{
title: 'ABC',
key: '0-1',
children: [
{
title: 'leaf 1-0',
key: '0-1-0',
isLeaf: true,
},
{
title: 'leaf 1-1',
key: '0-1-1',
isLeaf: true,
},
],
},
];
const searchString = 'ANUR';
const filtered = treeData.filter((datum) => {
const filteredChilds = datum.children.filter((child) => child.title.toLowerCase().includes(searchString.toLowerCase()));
return filteredChilds.length > 0;
});
console.log(filtered);
If you want to get these objects and only matched child objects try this:
const treeData = [{
title: 'Orange',
key: '0-0',
children: [{
title: 'Anu',
key: '0-0-0',
isLeaf: true,
},
{
title: 'Anurag',
key: '0-0-1',
isLeaf: true,
},
],
},
{
title: 'ABC',
key: '0-1',
children: [{
title: 'leaf 1-0',
key: '0-1-0',
isLeaf: true,
},
{
title: 'leaf 1-1',
key: '0-1-1',
isLeaf: true,
},
],
},
];
const searchString = 'ANUR';
const filtered = treeData.map((datum) => {
const filteredChilds = datum.children.filter((child) => child.title.toLowerCase().includes(searchString.toLowerCase()));
return {
...datum,
children: filteredChilds
};
}).filter(datum => datum.children.length > 0);
console.log(filtered);
If you want to get only matched child objects try this:
const treeData = [{
title: 'Orange',
key: '0-0',
children: [{
title: 'Anu',
key: '0-0-0',
isLeaf: true,
},
{
title: 'Anurag',
key: '0-0-1',
isLeaf: true,
},
],
},
{
title: 'ABC',
key: '0-1',
children: [{
title: 'leaf 1-0',
key: '0-1-0',
isLeaf: true,
},
{
title: 'leaf 1-1',
key: '0-1-1',
isLeaf: true,
},
],
},
];
const searchString = 'ANUR';
const filtered = treeData.reduce((acc, datum) => acc.concat(datum.children.filter((child) => child.title.toLowerCase().includes(searchString.toLowerCase()))), []);
console.log(filtered);

How to replace values in my array of objects using key from other object in another array

I have 2 arrays of objects
NOTE: status and original-language can't be set manually as they change all the time, these are custom fields. The slugs from fields make up all custom fields.
const items = [
{
name: 'Surviving the Game',
status: 'ffdb29ba075fcbc0b71295c31a13d64f',
original-language: 'b4ebbe06702794d1cf375274197267b2',
},
{
name: 'Some Movie',
status: 'cd53c082c6ca9e7d3ec66890e66c01f3',
original-language: '7a1cac74217747933bb3915888dea090',
},
];
const fields = [
{
slug: 'status',
options: [
{
name: 'Released',
id: 'ffdb29ba075fcbc0b71295c31a13d64f',
},
{
name: 'Upcoming',
id: 'cd53c082c6ca9e7d3ec66890e66c01f3',
},
],
},
{
slug: 'original-language',
options: [
{
name: 'de',
id: 'b4ebbe06702794d1cf375274197267b2',
},
{
name: 'en',
id: '7a1cac74217747933bb3915888dea090',
},
],
},
];
status and original-language in [items] have an id value which matches an option in the corresponding fields array.
I am trying to return a new array for [items] with the name from options with the matching id.
eg:
[
{
name: 'Surviving the Game',
status: 'Released',
original-language: 'de',
},
{
name: 'Some Movie',
status: 'Upcoming',
original-language: 'en',
},
];
How would I go about this with ES6/7?
I am not sure where to start
I would accomplish this by creating a lookup object that houses lookups for both your statuses and languages. You can then use this lookup object when mapping through your items.
var items = [
{
name: 'Surviving the Game',
status: 'ffdb29ba075fcbc0b71295c31a13d64f',
"original-language": 'b4ebbe06702794d1cf375274197267b2'
},
{
name: 'Some Movie',
status: 'cd53c082c6ca9e7d3ec66890e66c01f3',
"original-language": '7a1cac74217747933bb3915888dea090'
}
];
var fields = [
{
slug: 'status',
options: [
{
name: 'Released',
id: 'ffdb29ba075fcbc0b71295c31a13d64f'
},
{
name: 'Upcoming',
id: 'cd53c082c6ca9e7d3ec66890e66c01f3'
}
]
},
{
slug: 'original-language',
options: [
{
name: 'de',
id: 'b4ebbe06702794d1cf375274197267b2'
},
{
name: 'en',
id: '7a1cac74217747933bb3915888dea090'
}
]
}
];
const lookup = {};
fields.forEach(field => {
lookup[field.slug] = field.options.reduce((all, option) => ({
...all,
[option.id]: option.name
}), {})
});
const translatedItems = items.map(item => {
return Object.entries(item)
.reduce((all, [key, val]) => ({
...all,
[key]: lookup[key] ? lookup[key][val] : val
}),{});
});
console.log(translatedItems);
I'd define a function that obtains the value for a field, like so:
function valueForField(field, id) {
const field = fields.find((itemfields) => itemfields.slug === field);
if(!field) return null;
const option = field.options.find(option => option.id === id);
if(!option) return null;
return option.name;
}
This can then be used like so:
const newItems = items.map(item => {
const { name } = item;
const newItem = {name};
newItem["original-language"] = valueForField('original-language', item["original-language"]);
newItem.status = valueForField('status', item.status);
return newItem;
});
Use map to create a new array of objects having name, status and originalLanguage fields along with the find method to get the name from fields for every status identifier.
const items = [{
name: 'Surviving the Game',
status: 'ffdb29ba075fcbc0b71295c31a13d64f',
originalLanguage: 'b4ebbe06702794d1cf375274197267b2',
},
{
name: 'Some Movie',
status: 'cd53c082c6ca9e7d3ec66890e66c01f3',
originalLanguage: '7a1cac74217747933bb3915888dea090',
},
];
const fields = [{
slug: 'status',
options: [{
name: 'Released',
id: 'ffdb29ba075fcbc0b71295c31a13d64f',
},
{
name: 'Upcoming',
id: 'cd53c082c6ca9e7d3ec66890e66c01f3',
},
],
},
{
slug: 'original-language',
options: [{
name: 'de',
id: 'b4ebbe06702794d1cf375274197267b2',
},
{
name: 'en',
id: '7a1cac74217747933bb3915888dea090',
},
],
},
],
newArr = items.map(i => ({
name: i.name,
status: fields.find(f => f.slug == 'status').options.find(o => o.id == i.status).name,
originalLanguage: fields.find(f => f.slug == 'original-language').options.find(l => l.id == i.originalLanguage).name
}));
console.log(newArr);

Find element inside tree and change it's parents' properties values

I want to find an element inside a tree by its property value and then move back to the beginning of the tree and set isExpanded property of each of element's parents to true using JS.
So I have this object:
elementsArray = [
{
name: 'test_1',
isExpanded: false,
expandElements: [
{
name: 'test_1_1',
isExpanded: false,
expandElements: [
{
name: 'test_1_1_1'
},
{
name: 'test_1_1_2'
}
]
},
{
name: 'test_1_2',
isExpanded: false,
expandElements: [
{
name: 'test_1_2_1'
},
{
name: 'test_1_2_2'
}
]
}
]
},
{
name: 'test_2',
isExpanded: false,
expandElements: [
{
name: 'test_2_1',
isExpanded: false,
expandElements: [
{
name: 'test_2_1_1'
},
{
name: 'test_2_1_2'
}
]
},
{
name: 'test_2_2',
isExpanded: false,
expandElements: [
{
name: 'test_2_2_1'
},
{
name: 'test_2_2_2'
}
]
}
]
}
]
I want to find the element, which has property name: 'test_2_2_1' and set isExpanded property of each of its parents to true.
How it should work:
call a method expandParentElements(elementsArray, 'test_2_2_1')
elementsArray changed to:
elementsArray = [
{
name: 'test_1',
isExpanded: false,
expandElements: [
{
name: 'test_1_1',
isExpanded: false,
expandElements: [
{
name: 'test_1_1_1'
},
{
name: 'test_1_1_2'
}
]
},
{
name: 'test_1_2',
isExpanded: false,
expandElements: [
{
name: 'test_1_2_1'
},
{
name: 'test_1_2_2'
}
]
}
]
},
{
name: 'test_2',
isExpanded: **true**,
expandElements: [
{
name: 'test_2_1',
isExpanded: false,
expandElements: [
{
name: 'test_2_1_1'
},
{
name: 'test_2_1_2'
}
]
},
{
name: 'test_2_2',
isExpanded: **true**,
expandElements: [
{
name: 'test_2_2_1'
},
{
name: 'test_2_2_2'
}
]
}
]
}
]
By now i've managed to only recursively move inside tree and find proper element. Here how it looks like:
let expandParentElements = (array, searchName) => {
return array.map(item => {
if (item.name === searchName) {
return item
} else if (item.expandElements) {
return expandParentElements(item.expandElements, searchName)
}
})
}
Here is a function that sets isExpanded on the path to the item. The item itself (e.g. when it is has subitems) will not get isExpanded set to true.
This function does also not clear the isExpanded values that might have already been set to true before calling the function.
const expandParentElements = (array, searchName) =>
array && array.some(item => item.name === searchName ||
expandParentElements(item.expandElements, searchName) && (item.isExpanded = true)
);
// Sample data from question
const elementsArray = [{name: 'test_1',isExpanded: false,expandElements: [{name: 'test_1_1',isExpanded: false,expandElements: [{name: 'test_1_1_1'},{name: 'test_1_1_2'}]},{name: 'test_1_2',isExpanded: false,expandElements: [{name: 'test_1_2_1'},{name: 'test_1_2_2'}]}]},{name: 'test_2',isExpanded: false,expandElements: [{name: 'test_2_1',isExpanded: false,expandElements: [{name: 'test_2_1_1'},{name: 'test_2_1_2'}]},{name: 'test_2_2',isExpanded: false,expandElements: [{name: 'test_2_2_1'},{name: 'test_2_2_2'}]}]}];
expandParentElements(elementsArray, "test_2_2_1");
console.log(elementsArray);
You may want to also use a function to collapse all, i.e. set all isExpanded to false:
const collapseAll = (array) =>
array.filter(item => item.expandElements)
.forEach(item => (item.isExpanded = false, collapseAll(item.expandElements)));
elementsArray.forEach(element => {
let updateParent = element.expandElements.reduce(((prevFound, expandElement) => {
let found = expandElement.expandElements.some(obj => obj.name === "test_1_1_1")
expandElement.isExpanded = found
return prevFound || found
}), false)
element.isExpanded = updateParent
})

Efficient way to do the filter using loadash or any other library?

I am filtering array whenever checkboxes are checked. There are totally 7 checkboxe each is associated with an object.
here is my code,
if (this.deliveryConcession[0].checked) {
this.allItems = this.allItems.filter(fil => fil.deliveryconcession.readytoship === this.deliveryConcession[0].checked);
}
if (this.deliveryConcession[1].checked) {
this.allItems = this.allItems.filter(fil => fil.deliveryconcession.instantdownload === this.deliveryConcession[1].checked);
}
if (this.deliveryConcession[2].checked) {
this.allItems = this.allItems.filter(fil => fil.deliveryconcession.unespecifiedshipment === this.deliveryConcession[2].checked);
}
if (this.seatConcession[0].checked) {
this.allItems = this.allItems.filter(fil => fil.seatingConcession.parking === this.seatConcession[0].checked);
}
if (this.seatConcession[1].checked) {
this.allItems = this.allItems.filter(fil => fil.seatingConcession.restrictedview === this.seatConcession[1].checked);
}
if (this.seatConcession[2].checked) {
this.allItems = this.allItems.filter(fil => fil.seatingConcession.wheelchair === this.seatConcession[2].checked);
}
if (this.seatConcession[3].checked) {
this.allItems = this.allItems.filter(fil => fil.seatingConcession.alcoholFree === this.seatConcession[3].checked);
}
here is my objects for filter,
seatConcession = [
{ id: 1, name: 'Parking pass included', checked: false },
{ id: 2, name: 'Unrestricted view', checked: false },
{ id: 3, name: 'Wheel chair accessible', checked: false },
{ id: 4, name: 'Without age restrictions', checked: false }
];
deliveryConcession = [
{ id: 1, name: 'Ready to ship(paper)', checked: false },
{ id: 2, name: 'Instant download(e-ticket)', checked: false },
{ id: 3, name: 'Unspecified shipment(paper)', checked: false }
];
how can i improve the above with simple loadash filter or another way?
let keys = [
["readytoship", "deliveryConcession"],
["instantdownload", "deliveryConcession"],
/* and so on, make sure to order */
];
this.allItems.filter(item => {
return keys.every((arr, i) => {
let [k, attr] = arr;
return item[attr][k] === this[attr][i].checked;
});
});
You will need to order the keys array appropriately. But now it's a two-liner. Other than let and the arrow functions this is all valid ES 5, no lodash required.
EDIT
Since you still haven't actually posted the relevant code this is still something of a stab in the dark, but taking your sample input of
seatConcession = [
{ id: 1, name: 'Parking pass included', checked: false },
{ id: 2, name: 'Unrestricted view', checked: false },
{ id: 3, name: 'Wheel chair accessible', checked: false },
{ id: 4, name: 'Without age restrictions', checked: false }
];
deliveryConcession = [
{ id: 1, name: 'Ready to ship(paper)', checked: false },
{ id: 2, name: 'Instant download(e-ticket)', checked: false },
{ id: 3, name: 'Unspecified shipment(paper)', checked: false }
];
And assuming you have a list of which checkboxes are checked that is ordered in the same order as the objects like so
let checked = [true, false, true, true, false /* etc */];
You want to do something like this:
let filtered = seatConcessions
.concat(deliveryConcession)
.filter((obj, i) => checked[i]);
You will have to adapt this to your specific case (again, since the sample input you put up is different than the code you wrote), but is a pattern for doing this in general.

Categories