appu
appu

Reputation: 481

Fix: JS recursive function to get the nested (multilevel) child objects as array of objects

I want to get the output as:

options: [{
  id: 'parent1',
  label: 'parent1',
  children: [{
    id: 'child1',
    label: 'child1',
    children: [{
      id: 'lastChild1',
      label: 'lastChild1',
    }]
  }, { 
    id: 'child2',
    label: 'child2',
    children: [{
      id: 'lastChild2',
      label: 'lastChild2',
    }]
  }]
}]

However, the output from getOptions() is in the format where the children property array of parent1 object contain only the second child in the above format, first child is kind of overwritten or not visited by the for..in loop in the recurseList().

Can anyone fix the code to output the first child child1 along with child2 as well, basically any level of nesting.

var myObj = {
  parent1: {
    child1: {
      lastChild1: { test: 'cool'}
    },
    child2: {
      lastChild2: { test: 'cool'}
    }
  },
  parent2: {
    child2_1: {
      lastChild2_1: { test: 'cool'}
    },
    child2_2: {
      lastChild2_2: { test: 'cool'}
    }
  }
}

var result = getOptions(myObj)
console.log('result', result)

function getOptions(obj) {
  var options = []
  for (key in obj) {
    var data = recurseList(obj[key])
    options.push(data)
  }
  return options
}

function recurseList(obj) {
  let data= {}
  let option= []
  for (key in obj) {
    data.id = key
    data.label = key
    data.children = []

    if(obj[key] instanceof Object) {
      var val = recurseList(obj[key])
      data.children.push(val)
    }
  }
  return data
}

Actually, I want data from my firebase real-time-database as show in the image below:

firebase real-time-database snapshot

to be in the format for this vuejs plugin: https://vue-treeselect.js.org

Thanks

Upvotes: 0

Views: 1281

Answers (3)

Mulan
Mulan

Reputation: 135227

variable depth

Here's an adaptation to Scott's (wonderful) answer that allows you to convert your nested structure to a user-controlled depth; convertUntil -

  1. If the input, o, is not an object, (base case) there is nothing to convert, return the input
  2. Otherwise, (inductive) the input is an object. If, the object passes the user-supplied test, stop nesting and return children: {}
  3. Otherwise, (inductive) the input is an object but it does not pass the user-supplied test. Map over the input object and create a level of our output structure. Recur convertUntil on each object value.

Numbered comments above correspond to the code below -

const identity = x =>
  x

const convertUntil = (test = identity, o = {}) =>
  Object (o) !== o  // 1
    ? o
: test (o)          // 2
    ? {}
: Object
    .entries (o)    // 3
    .map
      ( ([ k, v ]) =>
          ({ id: k, label: k, children: convertUntil (test, v) })
      )

const myObj =
  { parent1:
      { child1: { lastChild1: { test: 'cool' } }
      , child2: { lastChild2: { test: 'cool' } }
      }
  , parent2:
      { child2_1: { lastChild2_1: { test: 'cool' } }
      , child2_2: { lastChild2_2: { test: 'cool' } }
      }
  }

console .log (convertUntil (x => x.test === "cool", myObj))


While I prefer this more consistent data structure, I can understand if you don't like the empty children: {} created above. With a slight modification, we can remove empty children properties -

const identity = x =>
  x

const convertUntil = (test = identity, o = {}) =>
  Object (o) !== o
    ? o
: Object
    .entries (o)
    .map
      ( ([ k, v ]) =>
          test (v) // <-- test here
            ? { id: k, label: k } // <-- no children
            : { id: k, label: k, children: convertUntil (test, v) }
      )

const myObj =
  { parent1:
      { child1: { lastChild1: { test: 'cool' } }
      , child2: { lastChild2: { test: 'cool' } }
      }
  , parent2:
      { child2_1: { lastChild2_1: { test: 'cool' } }
      , child2_2: { lastChild2_2: { test: 'cool' } }
      }
  }

console .log (convertUntil (x => x.test === "cool", myObj))

But watch out for -

console .log (convertUntil (x => x.test === "cool", { test: "cool" }))
// [ { id: "test", label: "test", children: "cool" } ]

fixed depth

Another option would be to convert the nested structure to a specified depth -

const identity = x =>
  x

const convert = (o = {}, depth = 0) =>
  Object (o) !== o
    ? o
: Object
    .entries (o)
    .map
      ( ([ k, v ]) =>
          depth === 0 // <-- depth test
            ? { id: k, label: k } // <-- no children
            : { id: k, label: k, children: convert (v, depth - 1) } // <-- depth minus one
      )

const myObj =
  { parent1:
      { child1: { lastChild1: { test: 'cool' } }
      , child2: { lastChild2: { test: 'cool' } }
      }
  , parent2:
      { child2_1: { lastChild2_1: { test: 'cool' } }
      , child2_2: { lastChild2_2: { test: 'cool' } }
      }
  }

// show various depths
for (const d of [ 0, 1, 2 ])
  console .log (`depth: ${d}`, convert (myObj, d))


combined technique

Per Scott's comment, the techniques can be combined into a single solution. This allows the user to continue conversion based on the object's properties or a specified depth level -

const identity = x =>
  x

const convertUntil = (test = identity, o = {}, depth = 0) =>
  Object (o) !== o
    ? o
: Object
    .entries (o)
    .map
      ( ([ k, v ]) =>
          test (v, depth) // <-- include depth in test
            ? { id: k, label: k }
            : { id: k, label: k, children: convertUntil (test, v, depth + 1) } // <-- depth plus one
      )

const myObj =
  { parent1:
      { child1: { lastChild1: { test: 'cool' } }
      , child2: { lastChild2: { test: 'cool' } }
      }
  , parent2:
      { child2_1: { lastChild2_1: { test: 'cool' } }
      , child2_2: { lastChild2_2: { test: 'cool' } }
      }
  }

console .log (convertUntil ((_, depth) => depth === 2, myObj))

Upvotes: 2

Scott Sauyet
Scott Sauyet

Reputation: 50787

If you don't need to distinguish the test node from the other nodes, then I think this is straightforward:

const convert = (obj) =>
  Object .entries (obj) .map (([k, v]) => ({
    id: k, 
    label: k,
    ...(typeof v == 'object' ? {children: convert (v)} : {})
  }))

const myObj = {
  parent1: {child1: {lastChild1: { test: 'cool'}}, child2: {lastChild2: { test: 'cool'}}},
  parent2: {child2_1: {lastChild2_1: { test: 'cool'}}, child2_2: {lastChild2_2: { test: 'cool'}}}
}

console .log (
  convert (myObj)
)

I'm guessing that distinguishing that deepest node would make this significantly more complex.

Update

Ok, so it's not that much more complex, if the condition is that the object has no properties which are themselves objects. (This is still not clear, and the requested output sample and the image posted as a comment on another answer seem to disagree. But that would be my best guess.) We can do this with either an inline test:

const convert = (obj) =>
  Object .entries (obj) .map (([k, v]) => ({
    id: k, 
    label: k,
    ...((typeof v == 'object' && Object .values (v) .some (o => typeof o == 'object'))
         ? {children: convert (v)} 
         : {}
       )
  }))

or with a helper function:

const hasObjectProperties = (obj) =>
  Object .values (obj) .some (o => typeof o == 'object')

const convert = (obj) =>
  Object .entries (obj) .map (([k, v]) => ({
    id: k, 
    label: k,
    ...((typeof v == 'object' && hasObjectProperties(v))
         ? {children: convert (v)} 
         : {}
       )
  }))

Using the latter, the code becomes:

const hasObjectProperties = (obj) =>
  Object .values (obj) .some (o => typeof o == 'object')

const convert = (obj) =>
  Object .entries (obj) .map (([k, v]) => ({
    id: k, 
    label: k,
    ...((typeof v == 'object' && hasObjectProperties(v))
         ? {children: convert (v)} 
         : {}
       )
  }))

const myObj = {
  parent1: {child1: {lastChild1: { test: 'cool'}}, child2: {lastChild2: { test: 'cool'}}},
  parent2: {child2_1: {lastChild2_1: { test: 'cool'}}, child2_2: {lastChild2_2: { test: 'cool'}}}
}

console .log (
  convert (myObj)
)

Upvotes: 1

Daniyal Lukmanov
Daniyal Lukmanov

Reputation: 1229

const myObj = {
        parent1: {
            child1: {
                lastChild1: { test: 'cool'}
            },
            child2: {
                lastChild2: { test: 'cool'}
            }
        },
        parent2: {
            child2_1: {
                lastChild2_1: { test: 'cool'}
            },
            child2_2: {
                lastChild2_2: { test: 'cool'}
            }
        }
    }


function getOptions(obj) {
    return Object.keys(obj).reduce((acc, cur) => {
        acc.push({
          id: cur,
          label: cur,
          children: recurseList(obj[cur])
        })
        return acc;
    }, [])
}
  
function recurseList(obj) {
    return Object.keys(obj).reduce((acc, cur) => {
        if(obj[cur] instanceof Object) {
            let data = {
                id: cur,
                label:cur
            }      
            const children = recurseList(obj[cur]);
            If(children.length) {
                  data.children = children
            }
            acc.push(data)
        }
        return acc;
    }, [])
}

var result = getOptions(myObj)
console.log('result', result)

The problem is that you always use empty children array in a loop. And also you are not using your very first key parent1 to push to your result array.

Upvotes: 3

Related Questions