Hitting a wall with this one, thought I would post it here in case some kind soul has come across a similar one. I have some data that looks something like this:
const input = [ { value: 'Miss1', children: [ { value: 'Miss2' }, { value: 'Hit1', children: [ { value: 'Miss3' } ] } ] }, { value: 'Miss4', children: [ { value: 'Miss5' }, { value: 'Miss6', children: [ { value: 'Hit2' } ] } ] }, { value: 'Miss7', children: [ { value: 'Miss8' }, { value: 'Miss9', children: [ { value: 'Miss10' } ] } ] }, { value: 'Hit3', children: [ { value: 'Miss11' }, { value: 'Miss12', children: [ { value: 'Miss13' } ] } ] }, { value: 'Miss14', children: [ { value: 'Hit4' }, { value: 'Miss15', children: [ { value: 'Miss16' } ] } ] }, ];
I don't know at run time how deep the hierarchy will be, i.e. how many levels of objects will have a children array. I have simplified the example somewhat, I will actually need to match the value properties against an array of search terms. Let's for the moment assume that I am matching where value.includes('Hit')
.
I need a function that returns a new array, such that:
Every non-matching object with no children, or no matches in children hierarchy, should not exist in output object
Every object with a descendant that contains a matching object, should remain
All descendants of matching objects should remain
I am considering a 'matching object' to be one with a value
property that contains the string Hit
in this case, and vice versa.
The output should look something like the following:
const expected = [ { value: 'Miss1', children: [ { value: 'Hit1', children: [ { value: 'Miss3' } ] } ] }, { value: 'Miss4', children: [ { value: 'Miss6', children: [ { value: 'Hit2' } ] } ] }, { value: 'Hit3', children: [ { value: 'Miss11' }, { value: 'Miss12', children: [ { value: 'Miss13' } ] } ] }, { value: 'Miss14', children: [ { value: 'Hit4' }, ] } ];
Many thanks to anyone who took the time to read this far, will post my solution if I get there first.
One can use filter() function in JavaScript to filter the object array based on attributes. The filter() function will return a new array containing all the array elements that pass the given condition. If no elements pass the condition it returns an empty array.
The filter() method creates a new array filled with elements that pass a test provided by a function. The filter() method does not execute the function for empty elements. The filter() method does not change the original array.
Using .filter()
and making a recursive call as I described in the comment above is basically what you need. You just need to update each .children
property with the result of the recursive call before returning.
The return value is just the .length
of the resulting .children
collection, so if there's at least one, the object is kept.
var res = input.filter(function f(o) { if (o.value.includes("Hit")) return true if (o.children) { return (o.children = o.children.filter(f)).length } })
const input = [ { value: 'Miss1', children: [ { value: 'Miss2' }, { value: 'Hit1', children: [ { value: 'Miss3' } ] } ] }, { value: 'Miss4', children: [ { value: 'Miss5' }, { value: 'Miss6', children: [ { value: 'Hit2' } ] } ] }, { value: 'Miss7', children: [ { value: 'Miss8' }, { value: 'Miss9', children: [ { value: 'Miss10' } ] } ] }, { value: 'Hit3', children: [ { value: 'Miss11' }, { value: 'Miss12', children: [ { value: 'Miss13' } ] } ] }, { value: 'Miss14', children: [ { value: 'Hit4' }, { value: 'Miss15', children: [ { value: 'Miss16' } ] } ] }, ]; var res = input.filter(function f(o) { if (o.value.includes("Hit")) return true if (o.children) { return (o.children = o.children.filter(f)).length } }) console.log(JSON.stringify(res, null, 2))
Note that .includes()
on a String is ES7, so may need to be patched for legacy browsers. You can use the traditional .indexOf("Hit") != -1
in its place.
To not mutate the original, create a map function that copies an object and use that before the filter.
function copy(o) { return Object.assign({}, o) } var res = input.map(copy).filter(function f(o) { if (o.value.includes("Hit")) return true if (o.children) { return (o.children = o.children.map(copy).filter(f)).length } })
To really squeeze the code down, you could do this:
var res = input.filter(function f(o) { return o.value.includes("Hit") || o.children && (o.children = o.children.filter(f)).length })
Though it gets a little hard to read.
Here's a function that'll do what you're looking for. Essentially it will test every item in arr
for a match, then recursively call filter on its children
. Also Object.assign
is used so that the underlying object isn't changed.
function filter(arr, term) { var matches = []; if (!Array.isArray(arr)) return matches; arr.forEach(function(i) { if (i.value.includes(term)) { matches.push(i); } else { let childResults = filter(i.children, term); if (childResults.length) matches.push(Object.assign({}, i, { children: childResults })); } }) return matches; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With