Home > OS >  Recursively build an object based on the keys & values from another?
Recursively build an object based on the keys & values from another?

Time:09-02

I want to programmatically build a dynamic data object based on a context object, like in the example below.

const context = {
   ...other props..., 
   groups: [ 
      {
         heading: 'basic',
         canHaveMultiple: false,  // data.basic = {username: { type: 'text', value: '' }, password: { type: 'password', value: ''}}
         inputs: [  
            {
              name: 'username',
              type: 'text',
              placeholder: 'username',
            },
            {
              name: 'password',
              type: 'password',
              placeholder: 'password',
            },
         ],
      },
      {
         heading: 'about',
         canHaveMultiple: false, // data.about = {about: { type: 'textarea', value: '' }}
         inputs: [  
            {
              name: 'about',
              type: 'textarea',
              placeholder: 'about',
              canHaveMultiple: false,
            },
         ],
      },
      {
         heading: 'hobbies',
         canHaveMultiple: true,  // data.hobbies = { model: { title: {type: 'text', value: ''}, description: {type: 'textarea', value: ''} }, values: [ { title: {type: 'text', value: ''}, description: {type: 'textarea', value: ''} }]
         inputs: [  
            {
              name: 'title',
              type: 'text',
              placeholder: 'about',
              canHaveMultiple: false,
            },
            {
              name: 'description',
              type: 'description',
              placeholder: null,
              canHaveMultiple: false,
            },
         ],
      },
      {
         heading: 'friends',
         canHaveMultiple: true, // data.friends = { model: {title: {type: 'text', value: '' }, description: { type: 'textarea', value: '' }} }, values: [{ name: {type: 'text', value: ''},hobbies: [{ title: {type: 'text', value: ''}, description: {type: 'textarea', value: ''}} }] }
         inputs: [
          {
           name: 'name',
           type: 'text',
           placeholder: 'this is fine',
           canHaveMultiple: false
          },
          {
            name: 'hobbies',
            type: 'nested',
            canHaveMultiple: true,
            inputs: [
             {
               name: 'title',
               type: 'textarea',
               placeholder: 'about',
               canHaveMultiple: false,
             },
             {
               name: 'description',
               type: 'textarea',
               placeholder: 'about',
               canHaveMultiple: false,
             },
           ]
         }
      ],
    },
  ],
}

The output data should be something like so:

data: {
  basic: {
      username: {
         type: 'text',
         value: '',
      },
      password: {
         type: 'password',
         value: ''
      }
  },
  about: {
     about: {
        type: 'textarea',
        value: '',
     }
  },
  hobbies: {
     model: {
       title: { 
          type: 'text',
          value: '',
       },
       description: {
          type: 'textarea',
          value: '',
       }
     },
     values: [
         {
            title: {
               type: 'text',
               value: '',
            },
            description: {
               type: 'textarea',
               value: '',
            }
         } 
     ]
  },
  friends: {
     model: {
       name: { 
         type: 'text',
         value: '',
       },
       hobbies: {
           title: { 
              type: 'text',
              value: '',
           },
           description: {
              type: 'textarea',
              value: '',
           }
       }
     },
     values: [ 

     ]
  },
}

In essence,

  1. groups[int].heading becomes the top level property of the data object, and
  2. each group along with each of the child from inputs has a canHaveMultiple property, which distinguishes whether the resulting object structure will be either:

canHaveMultiple == false

group.input.name: {
  group.type,
  value: ''
}

OR

canHaveMultiple == true

{  
  model: { 
     group.input.name: {
       type: group.input.type,
       value: ''
     }, 
     ... etc      
  },
  values: [{
     group.input[0].name: {
        type: group.input[0].type,
        value: '',
     },
     group.input[1].name: {
        type: group.input[1].type,
        value: '',
     }
  }]
}

The model is there so that I can easily push a new copy of that object into the values array.

So here is my question:

Is there a way to recursively do this so that the program will create the data object from the context object, and keep looking down the context object chain for any 'nested' type (also within the inputs array) until there is none left? Is this do-able and efficient or am I thinking and going about this the wrong way?

*PS: I have been trying real hard and wrecking my head for a few days now on this but I cannot seem to get it working for Nested Objects beyond 3 levels because I am a newbie in recursion. Please help me :(

CodePudding user response:

This function first group by heading (using reduce) then goes recursively over the inputs fields. That is if an input has inputs we loop that too.

const context={groups:[{heading:"basic",canHaveMultiple:!1,inputs:[{name:"username",type:"text",placeholder:"username"},{name:"password",type:"password",placeholder:"password"},]},{heading:"about",canHaveMultiple:!1,inputs:[{name:"about",type:"textarea",placeholder:"about",canHaveMultiple:!1},]},{heading:"hobbies",canHaveMultiple:!0,inputs:[{name:"title",type:"text",placeholder:"about",canHaveMultiple:!1},{name:"description",type:"textarea",placeholder:null,canHaveMultiple:!1},]},{heading:"friends",canHaveMultiple:!0,inputs:[{name:"name",type:"text",placeholder:"this is fine",canHaveMultiple:!1},{name:"hobbies",type:"nested",canHaveMultiple:!0,inputs:[{name:"title",type:"textarea",placeholder:"about",canHaveMultiple:!1},{name:"description",type:"textarea",placeholder:"about",canHaveMultiple:!1},]}]},]}

function transform(arr) {
  var result = arr.reduce(function(agg, item) {
    var heading = item.heading
    var canHaveMultiple = item.canHaveMultiple
    var parent;
    agg[heading] = {}
    if (canHaveMultiple === false) {
      parent = agg[heading]
    }
    if (canHaveMultiple === true) {
      agg[heading] = {
        model: {},
        values: []
      }
      parent = agg[heading]['model']
    }

    function do_inputs(parent, inputs) {
      inputs.forEach(function(input) {
        if (!input.inputs) {
          parent[input.name] = {
            type: input.type,
            value: ''
            // todo: placeholder and other properties
          }
        } else {
          // nested
          parent[input.name] = {}
          do_inputs(parent[input.name], input.inputs)
        }

      })
    }
    do_inputs(parent, item.inputs)

    return agg;
  }, {})

  return result;
}

console.log(transform(context.groups));
.as-console-wrapper {
  max-height: 100% !important;
}

CodePudding user response:

I have two snippets which might get you most of the way there.

The first one is perhaps too simple, ignoring your model/value part. But it should be easy to understand:

const convert = (xs) => Object .fromEntries (
  xs .map (({name, type, inputs = []}) => 
    [name, inputs .length ? convert (inputs) : {type, value: ''}]
  )
)

const restructure = (context) => ({
  data: Object .fromEntries (context .groups .map (
    ({heading, inputs}) => [heading, convert (inputs)]
  ))
})

const context = {other: "props", groups: [{heading: "basic", canHaveMultiple: !1, inputs: [{name: "username", type: "text", placeholder: "username"}, {name: "password", type: "password", placeholder: "password"}]}, {heading: "about", canHaveMultiple: !1, inputs: [{name: "about", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}, {heading: "hobbies", canHaveMultiple: !0, inputs: [{name: "title", type: "text", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "description", placeholder: null, canHaveMultiple: !1}]}, {heading: "friends", canHaveMultiple: !0, inputs: [{name: "name", type: "text", placeholder: "this is fine", canHaveMultiple: !1}, {name: "hobbies", type: "nested", canHaveMultiple: !0, inputs: [{name: "title", type: "textarea", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}]}]}

console .log (restructure (context))
.as-console-wrapper {max-height: 100% !important; top: 0}

The second one does add the model/value part at the expense of some additional complexity. And it's not clear to me if it's entirely correct, although I think it's close:

const convert = (xs) => Object .fromEntries (
  xs .map (({name, type, inputs = [], canHaveMultiple}) =>  [
    name, 
    canHaveMultiple  
      ? {model: convert (inputs), values: [convert (inputs)]} 
      : inputs.length ? convert (inputs) : {type, value: ''}
  ])
)

const restructure = (context) => ({
  data: Object .fromEntries (context .groups .map (
    ({heading, inputs, canHaveMultiple}) => [
      heading, 
      canHaveMultiple 
        ? {model: convert (inputs), values: [convert (inputs)]} 
        : convert (inputs)
    ]
  ))
})

const context = {other: "props", groups: [{heading: "basic", canHaveMultiple: !1, inputs: [{name: "username", type: "text", placeholder: "username"}, {name: "password", type: "password", placeholder: "password"}]}, {heading: "about", canHaveMultiple: !1, inputs: [{name: "about", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}, {heading: "hobbies", canHaveMultiple: !0, inputs: [{name: "title", type: "text", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "description", placeholder: null, canHaveMultiple: !1}]}, {heading: "friends", canHaveMultiple: !0, inputs: [{name: "name", type: "text", placeholder: "this is fine", canHaveMultiple: !1}, {name: "hobbies", type: "nested", canHaveMultiple: !0, inputs: [{name: "title", type: "textarea", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}]}]}

console .log (restructure (context))
.as-console-wrapper {max-height: 100% !important; top: 0}

In both of them, we could simplify a lot if you had a consistent interface. That is, if groups was called inputs and heading was called name, we could consolidate the two repetitive functions into a single one.

  • Related