This is my data structure:
const data = [
{
id: 'root',
name: 'root',
children: [
{
id: 'childA',
name: 'childA',
children: []
},
{
id: 'childB',
name: 'childB',
children: [
{
id: 'childB_A',
name: 'childB_A',
children: [
{
id: 'childB_A_A',
name: 'childB_A_A',
children: []
},
{
id: 'childB_A_B',
name: 'childB_A_B',
children: []
}
]
}
]
}
]
}
]
Now let's say I select childB_A_A
, this means the path to it is as follows root > childB > childB_A > childB_A_A
or with indexes 0 > 1 > 0 > 0
/ root[0].children[1].children[0].children[0]
This is the function I have now.
function findItem(children, indexes, item) {
if (indexes.length <= 0) {
return item
}
const [currPos] = indexes.splice(0, 1)
const currItem = children[currPos]
return findItem(currItem.children, indexes, currItem)
}
This is how I'm calling the function findItem(data, [0, 0])
(selecting childA
)
It successfully finds the item I'm searching. But that's not all I need, I also need to remove all children from the siblings of the found item.
So if I now have childB_A_A
selected and I select childA
then childB
's children need to be set to an empty array.
And I either need to mutate in place or return the new structure like this:
const data = [
{
id: 'root',
name: 'root',
children: [
{
id: 'childA',
name: 'childA',
children: []
},
{
id: 'childB',
name: 'childB',
children: []
}
]
}
]
I have tried using different approaches like passing the id
instead of the indexes path & recursively filtering the children like shown in this StackOverflow answer but it removes the sibling and I want to mutate it.
So yeah essentially what I need to do is:
- traverse nested array
- find item
- change all siblings to have an empty array
- return changed nested array
CodePudding user response:
UPDATE: This does not work after all
Sometimes the best way of solving a problem is not to try...
I finally figured it out by looking out how the StackOverflow answer I linked was returning the whole nested array and decided to follow a similar approach by mixing .map()
and my approach and some straightforward logic.
I commented the code to explain my logic
export function selectAndCleanChildren(items, indexes) {
let selectedItem = null
const mapItem = (item, indexes) => {
// if it's the last index then the selected child
// is one of the current item's children
if (indexes.length === 1) {
selectedItem = item.children[indexes[0]] // set the selected child for easy access
// loop over all children of the currentItem with .map()
item.children = item.children.map(item => {
// If: the child id is the same as the selectedItem.id then return the item unchanged
if (item.id === selectedItem.id) {
return item
}
// Else: keep the child as it is but set it's children to an empty array
return { ...item, children: [] }
})
// return the new item
return item
}
// remove the first index in-place
indexes.splice(0, 1)
// recursively call mapItem by passing the currentItem & rest of the indexes
return mapItem(item, indexes)
}
// map over items & call mapItem and pass the item & indexes
const cleanChildren = items.map(item => mapItem(item, indexes))
// return the clean children & selectedChild for ease of access
return { items: cleanChildren, selectedItem }
}
CodePudding user response:
I think if we break this down into some utility and helper functions, it's fairly straightforward.
We write a private _transform
function, which is given the target node and its parent node and recursively builds up a new tree (note: it does not mutate; we're not barbarians, after all!) searching for that parent node as it goes, with different behavior for that node and all others. Once it finds that node, it maps its children
choosing to keep the one that matches the target node and to clone the others with empty lists of children
.
The public transform
function is built on top of this and on top of two utility functions, findById
, and findParent
, themselves both built on the utility deepFind
, which finds a node matching the given predicate in a children
-based tree. transform
merely finds the target node, uses that to find the parent
node, and passes these into the private _transform
.
// utility functions
const deepFind = (pred) => ([x, ...xs] = []) =>
x && (pred (x) ? x : deepFind (pred) (x .children) || deepFind (pred) (xs))
const findById = (id) =>
deepFind ((x) => x.id == id)
const findParent = (target) =>
deepFind ((x => x .children .includes (target)))
// main function
const _transform = (parent, target) => (xs) =>
xs .map ((x) => ({
... x,
children: x == parent
? x .children .map (c => c == target ? c : {...c, children: []})
: _transform (parent, target) (x .children)
}))
// public function
const transform = (id, xs, target = findById (id) (xs), parent = findParent (target) (xs)) =>
_transform (parent, target) (xs)
// sample data
const data = [{id: 'root', name: 'root', children: [{id: 'childA', name: 'childA', children: []}, {id: 'childB', name: 'childB', children: [{id: 'childB_A', name: 'childB_A', children: [{id: 'childB_A_A', name: 'childB_A_A', children: []}, {id: 'childB_A_B', name: 'childB_A_B', children: []}]}]}]}]
// demo
console .log (transform ('childA', data))
.as-console-wrapper {max-height: 100% !important; top: 0}
Note that there is no error-checking for missing ids, but that shouldn't matter, as in that case, it returns something equivalent to your original tree.
This was written without looking at your answer. If you want to use an array of indices rather than an id
, it's only a minor change:
// utility functions
const deepFind = (pred) => ([x, ...xs] = []) =>
x && (pred (x) ? x : deepFind (pred) (x .children) || deepFind (pred) (xs))
const _findByIndices = (indices) => (xs) =>
indices .length == 0 ? xs : _findByIndices (indices .slice (1)) (xs .children [indices [0]])
const findByIndices = (indices) => (xs) => _findByIndices (indices) ({children: xs})
const findParent = (target) =>
deepFind ((x => x .children .includes (target)))
// main function
const _transform = (parent, target) => (xs) =>
xs .map ((x) => ({
... x,
children: x == parent
? x .children .map (c => c == target ? c : {...c, children: []})
: _transform (parent, target) (x .children)
}))
// public function
const transform = (indices, xs, target = findByIndices (indices) (xs), parent = findParent (target) (xs)) =>
_transform (parent, target) (xs)
// sample data
const data = [{id: 'root', name: 'root', children: [{id: 'childA', name: 'childA', children: []}, {id: 'childB', name: 'childB', children: [{id: 'childB_A', name: 'childB_A', children: [{id: 'childB_A_A', name: 'childB_A_A', children: []}, {id: 'childB_A_B', name: 'childB_A_B', children: []}]}]}]}]
// demo
console .log (transform ([0, 0], data))
.as-console-wrapper {max-height: 100% !important; top: 0}
The utility function deepFind
is extremely reusable, and findById
and findParent
are quite useful functions on their own, and you might find many uses for them. findByIndices
is perhaps a little less so, but it might come in handy elsewhere.
I need to reiterate that this does not modify your original array. It does some structural sharing, so some nodes will reference the same object between the two trees, but it is a new structure. If that won't work for you, then you might still do something similar to this mutating the tree, but you'll have to do that part without my help; I'm philosophically opposed to such mutation.