Programmatically find specific node and add property to deep nested object [duplicate] - javascript

This question already has answers here:
Dynamically set property of nested object
(28 answers)
Find by key deep in a nested array
(21 answers)
Closed last year.
Given an array like this, where the maximum depth can be 3 levels and where we don't know at what level the researched item could be:
const data = {
id: '1',
children: [
{
id: '2',
name: 'nameTest',
children: [
{
id: '3'
name: 'deepLevel'
}
]
}
}
how can I add a property to the third level knowing only the value 'deepLevel' ?
we are allowed to use lodash and strongly encouraged to use ES6.
the final dataStructure should be
Given an array like this, where the maximum depth can be of 3 levels:
const data = {
id: '1',
children: [
{
id: '2',
name: 'nameTest',
children: [
{
id: '3'
name: 'deepLevel'
addedProperty: true,
}
]
}
}

An approach was to separate the tasks of finding a nested item by a custom(izable) entry (key-value pair) and assigning additional custom data to the found item.
Thus one e.g. could implement two methods recursivelyFindItemByEntry which is based on self recursion and a simple assignToObjectWithFirstMatchingNestedEntry which assigns provided data to the result of the former function invocation ...
function recursivelyFindItemByEntry(obj, [key, value]) {
let item;
if (!!obj && (typeof obj === 'object')) {
if (
obj.hasOwnProperty(key) &&
(obj[key] === value)
) {
item = obj;
} else if (
obj.hasOwnProperty('children') &&
Array.isArray(obj.children)
) {
obj.children.some(child => {
item = recursivelyFindItemByEntry(child, [key, value]);
return !!item;
});
}
}
return item;
}
function assignToObjectWithFirstMatchingNestedEntry(obj, [key, value], data) {
Object.assign(
recursivelyFindItemByEntry(obj, [key, value]) ?? {},
data ?? {}
);
return obj;
}
const data = {
id: '1',
children: [{
id: '2',
name: 'nameTest',
children: [{
id: '3',
name: 'deepLevel',
}, {
id: '4',
name: 'deepLevel',
}],
}, {
id: '5',
name: 'nameTest',
children: [{
id: '6',
name: 'deepLevel',
}, {
id: '7',
name: 'deepLevelTarget',
// addedProperty: true,
}, {
id: '8',
name: 'deepLevel',
}],
}, {
id: '9',
name: 'nameTest'
}, {
id: '10',
name: 'nameTestTarget'
}, {
id: '11',
name: 'nameTest'
}],
};
console.log(
"recursivelyFindItemByEntry(data, ['name', 'deepLevelTarget']) ...",
recursivelyFindItemByEntry(data, ['name', 'deepLevelTarget'])
);
console.log(
"recursivelyFindItemByEntry(data, ['id', '10']) ...",
recursivelyFindItemByEntry(data, ['id', '10'])
);
console.log('\n');
console.log(
"recursivelyFindItemByEntry(data, ['id', 'foo']) ...",
recursivelyFindItemByEntry(data, ['id', 'foo'])
);
console.log(
"recursivelyFindItemByEntry(data, ['id', '1']) ...",
recursivelyFindItemByEntry(data, ['id', '1'])
);
console.log('\n');
console.log(
"assignToObjectWithFirstMatchingNestedEntry(data, ['name', 'deepLevelTarget']), { addedProperty: true } ...",
assignToObjectWithFirstMatchingNestedEntry(data, ['name', 'deepLevelTarget'], { addedProperty: true })
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Related

Remove duplicated objects in JavaScript Array not working

First of, I am aware that there are LOTS of answers on SO on this, but I am having some issues with them, that is why I post another question on this topic.
So here is my Array of Objects:
0: {id: 'lAYOUT', label: 'lAYOUT', items: 'val1'}
1: {id: 'tecst', label: 'tecst', items: 'val1'}
2: {id: 'tecst', label: 'tecst', items: 'val1'}
I am trying to filter out that there would be only 2 values, since there are 2 objects in array that are the same. I would like to make unique objects by items and label.
This is how I am trying to do it with lodash:
const filteredArray = uniq(nestedItems, (item, key, a) => item.items && item.label)
But it keeps returning me all 3 elements still.
I also tried it like this:
const filteredArray = [...new Set(nestedItems)]
Using Filter get the particular object value, index and array.
Using FindIndex get the particular array object. and compare filter object and findindex object, if it return false then push in new array! and make new unique array !
Try this code !
let arr = [{ id: 'lAYOUT', label: 'lAYOUT', items: 'val1' },
{ id: 'tecst', label: 'tecst', items: 'val1' },
{ id: 'tecst', label: 'tecst', items: 'val1' }];
let newArr = arr.filter((value, index, self) =>
index === self.findIndex((t) => (
t.label === value.label && t.items === value.items
))
);
console.log(newArr, 'newArr');
You can use hash grouping to filter by several keys:
const data = [{ id: 'lAYOUT', label: 'lAYOUT', items: 'val1' }, { id: 'tecst', label: 'tecst', items: 'val1' }, { id: 'tecst', label: 'tecst', items: 'val1' }];
const unique = Object.values(data.reduce((acc, obj) => {
const hash = `${obj.id}-${obj.label}`;
acc[hash] = obj;
return acc;
}, {}));
console.log(unique);
.as-console-wrapper{min-height: 100%!important; top: 0}
Same result with lodash
const data = [{ id: 'lAYOUT', label: 'lAYOUT', items: 'val1' }, { id: 'tecst', label: 'tecst', items: 'val1' }, { id: 'tecst', label: 'tecst', items: 'val1' }];
const result = _.uniqWith(data, (o1, o2) => `${o1.id}-${o1.label}` === `${o2.id}-${o2.label}`);
console.log(result);
.as-console-wrapper{min-height: 100%!important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.js" integrity="sha512-2iwCHjuj+PmdCyvb88rMOch0UcKQxVHi/gsAml1fN3eg82IDaO/cdzzeXX4iF2VzIIes7pODE1/G0ts3QBwslA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
const data = [
{ id: 'id1', label: 'label1', items: 'items1' },
{ id: 'id2', label: 'label2', items: 'items2' },
{ id: 'id1', label: 'label1', items: 'items2' }
];
const unique = (...keys) => [
...new Map(data.map(item => [keys.map(key => item[key]).join(), item])).values()
];
console.log(1, unique('label'));
console.log(2, unique('label','items'));

How to search a value from an json which has 2 level as return matches from both the object?

I have a array of object which has a inside array which need to be filtered and return array based on matches from both. search is (input) event, which executes on every key press.
stackblitz link stackblitz
list = [
{
id: 'abc',
data: [
{ key: '1', value: 'car' },
{ key: '2', value: 'bus' },
{ key: '3', value: 'bike' },
{ key: '4', value: 'truck' },
{ key: '5', value: 'jeep' },
],
},
{
id: 'def',
data: [
{ key: '1', value: 'car' },
{ key: '2', value: 'bicycle' },
{ key: '3', value: 'train' },
{ key: '4', value: 'aeroplane' },
{ key: '5', value: 'jeep' },
],
},
];
handleSearch = (event) => {
if (event.target.value.length > 0) {
const item = this.list[0].data.filter((items) =>
items.value.toLowerCase().includes(event.target.value.toLowerCase())
);
this.list[0].data = item;
} else {
this.list[0].data = this.orgList;
}
};
expect output
input = car
output = [
{
id: 'abc',
data: [
{ key: '1', value: 'car' },
],
},
{
id: 'def',
data: [
{ key: '1', value: 'car' },
],
},
];
input = truck
output =
[
{
id: 'abc',
data: [
{ key: '4', value: 'truck' },
],
},
];
const list = [{id: 'abc',data: [{ key: '1', value: 'car' },{ key: '2', value: 'bus' },{ key: '3', value: 'bike' },{ key: '4', value: 'truck' },{ key: '5', value: 'jeep' },],},{id: 'def',data: [{ key: '1', value: 'car' },{ key: '2', value: 'bicycle' },{ key: '3', value: 'train' },{ key: '4', value: 'aeroplane' },{ key: '5', value: 'jeep' },],},];
function search(arr, searchVal) {
return arr.map((item) => {
const data = item.data.filter(({ value }) => value === searchVal);
return { ...item, data };
})
.filter(({ data }) => data.length);
}
console.log(search(list, 'car'));
console.log(search(list, 'truck'));
.as-console-wrapper { max-height: 100% !important; top: 0 }
Angular demo
I know that I might be going a bit outside of the scope of your requirements here, but I just simply thought that it might be easier to do it like this.
I just thought that it might be somewhat more scalable this way, if you first flatten the structure, because for arguments sake, let's say that you're data structure needs to become more & more complex overtime, IDK, business requirements change. At least if you have some layer of abstraction to manage that, you can then filter on an array of objects quite simply, like I've done below.
Depending on your needs you may not even need to flatten the structure, it's just my opinion & experience states this to be an easier & more maintainable kinda solution to scale. If you're data structure dose evolve with complexity, where there may be nested structures, you could always look at using some clever little recursive function to flatten your structure.
It's worth also noting that I've added some validation to the search function, while it's probably not a requirement, it's not a bad idea to include such logic, where you could update state on your view model. You could include something like a toast notification, stating that the user has provided an invalid search term, you could be making a request to a server to get this data & you could say that there were no results, etc, I think you get the idea?
I hope that's helped & I'm sorry if I've gone a little OTT. šŸ˜…
const list = [
{
id: 'abc',
data: [
{ key: '1', value: 'car' },
{ key: '2', value: 'bus' },
{ key: '3', value: 'bike' },
{ key: '4', value: 'truck' },
{ key: '5', value: 'jeep' },
],
},
{
id: 'def',
data: [
{ key: '1', value: 'car' },
{ key: '2', value: 'bicycle' },
{ key: '3', value: 'train' },
{ key: '4', value: 'aeroplane' },
{ key: '5', value: 'jeep' },
],
},
];
const flattenStructure = data => {
return data.reduce((accumulator, item) => {
const items = item.data.reduce((vehicles, vehicle) => {
const modified = { ...vehicle, id: item.id };
return vehicles.concat(modified);
}, []);
return accumulator.concat(items);
}, []);
};
const search = (array, term) => {
const invalidTerm = term == null || typeof term != 'string' || term.replace(/ /g, '') == '';
const invalidArray = array == null || !Array.isArray(array);
if (invalidTerm || invalidArray) {
console.log("Invalid arguments provided.");
return array;
}
return flattenStructure(array).filter(vehicle => {
const match = vehicle.value.toLowerCase() == term.toLowerCase();
const contains = vehicle.value.toLowerCase().indexOf(term.toLowerCase()) > -1;
return match || contains;
});
};
console.log(search(list, 'car'));
console.log(search(list, 'truck'));
Generaly speaking, when dealing with filtering, avoid using same original array to display filtered results in template.
Concerning filtering function, this should do the trick:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
public list: any;
public orgList: any;
public filteredList: any;
ngOnInit() {
this.list = this.orgList = [
{
id: 'abc',
data: [
{ key: '1', value: 'car' },
{ key: '2', value: 'bus' },
{ key: '3', value: 'bike' },
{ key: '4', value: 'truck' },
{ key: '5', value: 'jeep' },
],
},
{
id: 'def',
data: [
{ key: '1', value: 'car' },
{ key: '2', value: 'bicycle' },
{ key: '3', value: 'train' },
{ key: '4', value: 'aeroplane' },
{ key: '5', value: 'jeep' },
],
},
];
}
filterData = (dataItem, term: string) => {
return dataItem.value.toLowerCase().indexOf(term.toLowerCase()) !== -1;
};
handleSearch = (event) => {
if (event.target.value.length === 0) {
this.filteredList = [];
return;
}
const term = event.target.value;
const temp = this.list.filter((fullItem) =>
fullItem.data.filter((dataItem) => this.filterData(dataItem, term))
);
this.filteredList = temp
.map((fullItem) => ({
...fullItem,
data: fullItem.data.filter((dataItem) =>
this.filterData(dataItem, term)
),
}))
.filter((fullItem) => fullItem.data.length > 0);
};
}

Getting parent node.name recursion is not working properly

I have an infinite tree:
const Data = [
{
id: '1',
name: 'hello',
children: [
{
id: '2',
name: 'world',
children: [
{
id: '3',
name: 'world',
children: [],
},
{
id: '4',
name: 'world',
children: [],
},
],
},
{
id: '5',
name: 'world',
children: [],
},
],
},
];
What I want to do is get the id and name of the path that leads to "world" and push it in to an array.
For example: the first path would be:
[
{ id: '1', name: 'hello' },
{ id: '2', name: 'world' },
]
second:
[
{ id: '1', name: 'hello' },
{ id: '2', name: 'world' },
{ id: '3', name: 'world' },
]
And then push those arrays into another array.
So my result would look like this:
const result = [
[
{ id: '1', name: 'hello' },
{ id: '2', name: 'world' },
],
[
{ id: '1', name: 'hello' },
{ id: '2', name: 'world' },
{ id: '3', name: 'world' },
],
[
{ id: '1', name: 'hello' },
{ id: '2', name: 'world' },
{ id: '4', name: 'world' },
],
[
{ id: '1', name: 'hello' },
{ id: '5', name: 'world' },
],
];
I have a recursive function:
const findPath = (input="world", data, visitedStack, dataStack) => {
return data.map((node) => {
visitedStack.push({ id: node.id, name: node.name });
if (node.name.toLowerCase().includes(input.toLowerCase())) {
dataStack.push([...visitedStack]);
}
return findPath(
input,
node.children,
visitedStack,
dataStack
);
});
};
But this is adding on all the paths it has visited, so the last array that is pushed into dataStack will look like this:
[
{ id: '1', name: 'hello' },
{ id: '2', name: 'world' },
{ id: '3', name: 'world' },
{ id: '4', name: 'world' },
{ id: '5', name: 'world' },
]
Not sure how to fix this. Or is this an incorrect approach?
The problem is that your visitedStack keeps growing, as you are eventually pushing all nodes unto it. Be aware that all recursive executions of your function get the same visitedStack to work with. So pushing [...visitedStack] is not going to push a path, but all nodes that had been visited before, which after a while do not represent a path any more.
If we stick with your function, then just make sure you don't push on visited permanently, but create a copy of that stack with the extra node, which will remain in the deeper recursion, but will not contaminate the whole rest of the execution. This way that extra node will not be there in the other, sibling paths:
const findPath = (input="world", data, visitedStack, dataStack) => {
return data.map((node) => {
let newStack = visitedStack.concat({ id: node.id, name: node.name });
if (node.name.toLowerCase().includes(input.toLowerCase())) {
dataStack.push(newStack);
}
return findPath(
input,
node.children,
newStack,
dataStack
);
});
};
Call as:
let result = [];
findPath("world", data, [], result);
console.log(result);
Alternative
I would however also address the following:
It is a bit odd that findPath does not return the result, but that the caller needs to provide the array in which the resulting paths should be collected. So I would suggest a function that returns the new array, not requiring the caller to pass that array as argument.
It is not useful to have a default value for a parameter, when other parameters following it, do not have a default value. Because, that means you anyway have to provide values for those other parameters, including the one that could have had a default value.
The paths that are returned still contain multiple references to the same objects. You do copy the objects into new objects, but as that new object sits in visitedStack, it will be reused when pushed potentially several times for deeper paths. So I would suggest making the object copies at the very last moment -- when the path is pushing on the result array.
Instead of repeatedly converting the input to lower case, do this only once.
Here is how you could write it:
function findPath(data, input="world") {
const result = [];
input = input.toLowerCase();
function recur(data, visitedStack) {
for (const node of data) {
const newStack = visitedStack.concat(node);
if (node.name.toLowerCase().includes(input)) {
result.push(newStack.map(o => ({id: o.id, name:o.name})));
}
recur(node.children, newStack);
}
}
recur(data, []);
return result;
}
const data = [{id: '1',name: 'hello',children: [{id: '2',name: 'world',children: [{id: '3',name: 'world',children: [],},{id: '4',name: 'world',children: [],},],},{id: '5',name: 'world',children: [],},],},];
const result = findPath(data);
console.log(result);

Javascript - Remove object from nested array

I have array of objects, each object must have key and title, but children is optional, and it can be nested, i can have children inside of children many times. I want to remove some object by provided key value (for example key 677). I tried with filter but i only remove first level. Also have tried recursion, but not sure if i did it right.
const data = [{
key: '1',
title: 'title 1',
children: [{
key: '098',
title: 'hey',
children: [{
key: '677',
title: 'child'
}]
}]
},
{
key: '123',
title: 'tile 111'
},
{
key: '345',
title: 'something'
}
];
const rem = '677';
const del = (el) => {
if (!el.children) {
return el.key !== rem;
} else {
if (el.key !== rem) {
del(el.children);
return el;
}
}
};
const res = data.filter((el) => {
return del(el);
});
console.log(res);
I guess your existing solution is like
const data = [
{
key: '1',
title: 'title 1',
children: [{
key: '098',
title: 'hey',
children: [{ key: '677', title: 'child'}]
}]
},
{ key: '123', title: 'tile 111' },
{ key: '345', title: 'something' }
];
function removeByKey(arr, removingKey){
return arr.filter( a => a.key !== removingKey);
}
So it works on the first level but not deeply.
Just change it like that will do the jobs
function removeByKey(arr, removingKey){
return arr.filter( a => a.key !== removingKey).map( e => {
return { ...e, children: removeByKey(e.children || [], removingKey)}
});
}
Little warning, children property will not be set to [] for every item not having any children.
So how it works? Well instead of keeping acceptable items as they are, we make a copy using {...e} that's equivalent to {key:e.key, title:e.title, children:e.children} in this case.
We know force to override the property children with removeByKey(e.children || [], removingKey), so we call the method recursively. Not the function works deeeply.
I would use a recursion approach with findIndex and splice. Using some will allow the code to exit without running through the entire tree.
const data = [{
key: '1',
title: 'title 1',
children: [{
key: '098',
title: 'hey',
children: [{
key: '677',
title: 'child'
}]
}]
},
{
key: '123',
title: 'tile 111'
},
{
key: '345',
title: 'something'
}
];
const removeKey = (data, key) => {
// look to see if object exists
const index = data.findIndex(x => x.key === key);
if (index > -1) {
data.splice(index, 1); // remove the object
return true
} else {
// loop over the indexes of the array until we find one with the key
return data.some(x => {
if (x.children) {
return removeKey(x.children, key);
} else {
return false;
}
})
}
}
console.log(removeKey(data, '677'))
console.log(JSON.stringify(data));
You can use some simple recursion to do the trick:
const data = [
{
key: '1',
title: 'title 1',
children: [
{
key: '098',
title: 'hey',
children: [{ key: '677', title: 'child'}]
}
]
},
{ key: '123', title: 'tile 111' },
{ key: '345', title: 'something' }
];
function removeByKey(key, arr) {
// loop through all items of array
for(let i = 0; i < arr.length; i++) {
// if array item has said key, then remove it
if(arr[i].key === key) {
arr.splice(i, 1);
} else if(typeof(arr[i].children) !== "undefined") {
// if object doesn't have desired key but has children, call this function
// on the children array
removeByKey(key, arr[i].children);
}
}
}
removeByKey('098', data);
console.log(data);
This may be a little easier to understand than the other answer provided.

how to sort array object based on another object

it possible to sort and rearrange an array that looks like this:
items:[{
id: '5',
name: 'wa'
},{
id: '3',
name: 'ads'
},{
id: '1',
name: 'fdf'
}]
to match the arrangement of this object:
Item_sequence: {
"5": {index: 1},
"1": { index: 0 }
}
Here is the output Iā€™m looking for:
items:[{
id: '1',
name: 'fdf'
},{
id: '5',
name: 'wa'
},{
id: '3',
name: 'ads'
}]
You could check if the index is supplied and if not take a lage value for sorting by delta of two items.
var data = { items: [{ id: '5', name: 'wa' }, { id: '3', name: 'ads' }, { id: '1', name: 'fdf' }] },
sequence = { 5: { index: 1 }, 1: { index: 0 } };
data.items.sort(({ id: a }, { id: b }) =>
(a in sequence ? sequence[a].index : Number.MAX_VALUE) -
(b in sequence ? sequence[b].index : Number.MAX_VALUE)
);
console.log(data.items);
.as-console-wrapper { max-height: 100% !important; top: 0; }
JavaScript specifically, First you have to apply loop to your array "items":
`
let newArr = [];
items.map(obj=>{
//obj will be the element of items array, here it is an object.
if(Item_sequence.obj[id] !== undefined) {
/*this condition will be satisfied when id from items array will be present as a
key in Item_sequence array*/
insertAt(newArr, Item_sequence.obj[id] , obj)
}
else{
newArr.push(obj);
}
})
//After checking on whole array here you assign a newArr value to items array.
items=newArr;
Hope that it will help you.

Categories

Resources