Create array of objects based off the same property names

Viewed 196

I have form inputs that are generated based off an XML file.

We keep a reference of nested elements in the XML file via data-* attributes which can later be used to build an object. For example:

<parent>
 <child>
  <grandchild1>first</grandchild1>
  <grandchild2>second</grandchild2>
 </child>
</parent>

Becomes

<input type="text" data-nest="parent.child.grandchild1" value="first"/> <input type="text" data-nest="parent.child.grandchild2" value="second"/>

When we submit the form, I create an object (with nested objects) based off of their data-nest attribute. The above would become

parent:{
 child:{
  grandchild1: first,
  grandchild2: second
  }
}

The issue that I am running into is where multiple of the same tag are found in the XML file, e.g.

<child>
 <grandchild>first</grandchild>
 <grandchildFriend>firstFriend</grandchildFriend>
</child>
<child>
 <grandchild>second</grandchild>
 <grandchildFriend>secondFriend</grandchildFriend>
</child>

When I create my object, I would like it so that if multiple occurrences of the same data-nest value are found, they are nested inside of an array in order to maintain the different values.

With the current setup, the second occurrence of the tag will understandably overwrite the first.

This is the final object structure I desire:

parent:{
 child:[
  {
   grandchild: first,
   grandchildFriend: firstFriend
  }, 
  {
   grandchild: second,
   grandchildFriend: secondFriend
  }
  ]
 }

TL;DR

I'd like to change an object to an array of nested objects if they have the same data-* attribute

Here's a fiddle of how the current code works to help give a better understanding.

4 Answers

replacing currentObj[lastKey] = value; with this:

if(typeof currentObj[lastKey] === 'undefined') {
    currentObj[lastKey] = value;
} else  {
    if(typeof currentObj[lastKey] !== 'array')
            currentObj[lastKey] = [currentObj[lastKey]];
    currentObj[lastKey].push(value);
}

the code will do the following: if currentObj[lastKey] is not set, you will create an element as always, otherwise, if it is already set and is a string the code will convert it into an array and then push into the arrays the subsequent (as many as you want) elements and the final result will be something similar to:

parent:{
...
 grandchild:[
   "first", "second"
  ]
...
 }

Edit in order to have a result in the format that you have requested, you need to do some more edit, like following:

var json = {};

config = {
  set: function(keyValueString) {

    var pair = keyValueString.split('=');

    // left of the = is the key path
    var keyPath = pair[0];

    // right of the = is the value to set
    var value = pair[1];

    // split keyPath into an array of keys
    var keys = keyPath.split('.');
    var key;

    var currentObj = json;

    // loop through all keys in the key path, except the last one.
    // this creates the object structure implied by the key path.
    // we want to do something different on the last iteration.
    for (var i=0; i < keys.length-1; i++) {

      // Get the current key we are looping
      key = keys[i];
      // if the requested level on the current object doesn't exist,
      // make a blank object.
      if (typeof currentObj[key] === 'undefined') {
        currentObj[key] = i === keys.length-2 ? []: {};
      }

      currentObj = currentObj[key];
    }

    // our loop doesn't handle the last key, because that's when we
    // want to set the actual value.
    var lastKey = keys[keys.length-1]

    // set the property of the deepest object to the value.
    var child = {};
    child[lastKey] = value;
    currentObj.push(child);
  }
};

// iterate through all of our inputs, and nest them based off their data-* attribute
$('input').each(function(){
    // set nest = value setup, e.g. parent.child.grandchild = first
  // we then break this 
    var key = $(this).attr('data-nest') + '=' + $(this).val();
  config.set(key);
});

// as a bonus - this is how I went about breaking multiple values within a single input into an array
function traverseObject(object){
  // iterate through the object
  for (var i in object){


    // if the next key exists, run again
    if(object[i] !== null && typeof(object[i])=="object"){
      traverseObject(object[i]);
    } else {
    var value = [object[i]];
     // if the value contains a comma
    if(value.toString().indexOf(',') > -1){
      // set the *nested* key to the 
      // value as an array by splitting at the commas
      object[i] =value.toString().split(',').filter(function(el){
        // remove the extra entry after splitting
        return el.length != 0;
      });
    }
    }
  }
}

traverseObject(json);

$('#test').on('click', function(){
    console.log(json)
});

Try this https://jsfiddle.net/ado7eLL9/19/

by updating the set method of config like this

if (typeof currentObj[lastKey] === 'undefined') {
   currentObj[lastKey] = value;
} else if(!Array.isArray(currentObj[lastKey])) {
  currentObj[lastKey] = [currentObj[lastKey], value];    
} else if(Array.isArray(currentObj[key])){
  currentObj[lastKey].push(value);
}

Update your config.set() as follows:

var json = {};

config = {
  set: function(keyValueString) {

    var pair = keyValueString.split('=');

    // left of the = is the key path
    var keyPath = pair[0];

    // right of the = is the value to set
    var value = pair[1];

    // split keyPath into an array of keys
    var keys = keyPath.split('.');
    var key;

    var currentObj = json;

    // loop through all keys in the key path, except the last one.
    // this creates the object structure implied by the key path.
    // we want to do something different on the last iteration.
    for (var i=0; i < keys.length-1; i++) {

      // Get the current key we are looping
      key = keys[i];
      // if the requested level on the current object doesn't exist,
      // make a blank object.
      if (typeof currentObj[key] === 'undefined') {
        currentObj[key] = {};
      }

      currentObj = currentObj[key];
    }

    // our loop doesn't handle the last key, because that's when we
    // want to set the actual value.
    var lastKey = keys[keys.length-1]

    
    if (typeof currentObj[lastKey] === 'undefined') {
      // set string if nothing exists
       currentObj[lastKey] = value;
       
    } else if(!Array.isArray(currentObj[lastKey])) {
      // if exists and is not an array, 
      // create an array with two items
      // previously stored value (string) with lastKey key
      // current value 
      var previous = { [lastKey]: currentObj[lastKey] };
      var current  = { [lastKey]: value };
      currentObj[lastKey] = [previous, current];  
      
    } else if(Array.isArray(currentObj[key])){
      // if exists and is an array
      // push the new object to it
      var current = { [lastKey]: value }
      currentObj[lastKey].push(current);
      
    }
  }
};

// iterate through all of our inputs, and nest them based off their data-* attribute
$('input').each(function(){
 // set nest = value setup, e.g. parent.child.grandchild = first
  // we then break this 
 var key = $(this).attr('data-nest') + '=' + $(this).val();
  config.set(key);
});

// as a bonus - this is how I went about breaking multiple values within a single input into an array
function traverseObject(object){
  // iterate through the object
  for (var i in object){
    var value = [object[i]];
    
    // EDIT: continue only if value is not an array
    if(!Array.isArray(object[i])) {
      // if the value contains a comma
      if(value.toString().indexOf(',') > -1){
        // set the *nested* key to the 
        // value as an array by splitting at the commas
        object[i] = value.toString().split(',').filter(function(el){
          // remove the extra entry after splitting
          return el.length != 0;
        });
      }
      // if the next key exists, run again
      if(object[i] !== null && typeof(object[i])=="object"){
        traverseObject(object[i]);
      }
    }
  }
}

traverseObject(json);

$('#test').on('click', function(){
 console.log(json)
});
#test{
  padding: 5px;
  border: 1px solid black;
  font-size: 15px;
  cursor: pointer;
  width: 150px;
  margin: 5px;
  text-align: center;
}
input{
  display: block;
  margin: 5px;
  padding: 5px;
  width: 150px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<label>Below are unique tags (working fine)</label>
<input type="text" data-nest="parent.child.grandchild1" value="gchild1-first"/>
<input type="text" data-nest="parent.child.grandchild2" value="gchild2-second"/>

<label>Below is multiple occurances of the same tag (find a way to break these into an array of objects)</label>
<input type="text" data-nest="parent.child.grandchild" value="gchild-first"/>
<input type="text" data-nest="parent.child.grandchild" value="gchild-second"/>

<label>Single array example</label>
<input type="text" data-nest="parent.child.array" value="this,is,an,array"/>

<div id="test">
  Test
</div>

Related