3

Recently I've found that I have had to create a object from attributes on a HTML tag. I am doing this in a AngularJS environment, so hyphenated attributes are converted to camelCase, but I could also do the same using data- attributes and dataset

So for example I have:

<element person-name="Grant" animation-jump="123" />

Which gives the object

{
   "personName" : "Grant",
   "animationJump" : "123"
{

My problem is that I then want to convert that camelCase object into a structured object:

{
  "person" : {
    "name" : "Grant" },
  "animation" : {
    "jump" : "123" }
}

I've created a JSFiddle of my QUint Unit Test https://jsfiddle.net/gdt3bonw/ It's actually working for the case I want which is only 1 level, but I would like to get it working for any number of levels because I foresee that it will be needed and so I can release the code publicly.

Luke T O'Brien
  • 2,565
  • 3
  • 26
  • 38
  • 1
    You'll want to have a look at [Fastest way to flatten / un-flatten nested JSON objects](http://stackoverflow.com/q/19098797/1048572) (except for using camelcase instead of dots to join/split nested property names) – Bergi Apr 02 '16 at 18:01

5 Answers5

2

We will loop through the keys of the object using reduce, building up the result. We decompose each key into its components, such as personName into person and name. We loop over these components, creating subobjects if they do not already exist. Finally, we add the final component to the innermost subobject as a property with the value in question.

Object.keys(input).reduce((result, key) => {
  var parts = key.match( /(^|[A-Z])[a-z]+/g) . map(part => part.toLowerCase());
  var leaf = parts.pop();
  var obj = result;

  parts.forEach(part => obj = obj[part] = obj[part] || {});
  obj[leaf] = input[key];

  return result;
}, {});
  • Have removed the ECMA bit - Do you think you could edit my JSFiddle? – Luke T O'Brien Apr 02 '16 at 17:56
  • You should test with `deepEqual`. –  Apr 02 '16 at 18:20
  • This solution breaks if there are two or more consecutive capitals in the camelCase attribute such as `"fancyGirlTakesARide"`. It only takes the last consequent capital into account. So no `a` object is created for the above attribute. – Redu Apr 03 '16 at 23:11
  • Sure, and it fails for `fancyGirlWritesCSS`. You have to decide what you want the behavior to be in that case. If you want individual upper-case letters to be individual properties, then replace the `+` after the `[a-z]` with a `*`. If you want segments to allow multiple adjacent upper case letters, then add a `+` after the `[A-Z]`. –  Apr 04 '16 at 02:36
  • Well i guess you decide either to separate the consecutive capitals or to treat them as one, but definitely not throw all of them away up until the one that's followed with a lowercase (if exists) This would be loss of information. So accordingly correct regexes are either `/(^|[A-Z])[a-z]*/g` or `/(^|[A-Z]+)[a-z]*/g` but not the one in the answer. – Redu Apr 04 '16 at 07:47
1

You can't use that in this way, and I don't think that it would be a logic proposal. Below I explain why it wouldn't.

obj[["animation","jump"]] = "123"

replace it with

obj["animation"]["jump"] = "123"

and it's all fine.

Why I don't support your idea?

  • It's messy to use, there is no style in doing that.
  • There is no logic in using an array as an object key
  • There is another way of calling an object item by key: using a dot, and that won't support your idea. I think everyone can imagine why.
boxHiccup
  • 128
  • 8
1

Why do you need to convert the attribute to camelCase in the first place..? Just do

function arrayToStructuredObject(obj,props){
  if (props.length){
    obj[props[0]] = props.length > 1 ? {} : value;
    arrayToStructuredObject(obj[props.shift()],props);
  }
  return obj;
}

var props = "animation-jump-tremendous-pinky".split("-"),
    value = "123", 
      obj = {},
     sobj = {};
sobj = arrayToStructuredObject(obj, props);

Besides i would like to remind that using the bracket notation to create a property is only possible if the reference that the bracket notation is used upon is predefined as an object. Such as

var o1; // <- undefined
o1["myProp"] = 1; // Uncaught TypeError: Cannot set property 'myProp' of undefined

while

var o2 = {}; // Object {}
o2["myProp"] = 1; // <- 1

then again

o2["myProp"]["myOtherProp"] = 2; // <- 2 but won't type coerce o2.myProp to Object

So speaking of proposals, i am not sure if utilizing bracket notation directly over undefined variables yet as another object creation pattern makes sense or not.

Well in any case one complete solution would be

var inp = {"personName" : "Grant", "animationJump" : "123", "fancyGirlTakesARide" : "987"},
 result = Object.keys(inp).reduce(function(p,c,i){
                                    var props = c.replace(/[A-Z]/g, m => "-" + m.toLowerCase()).split("-");
                                    return arrayToStructuredObject(p,props,inp[c])                    
                                  },{});

function arrayToStructuredObject(obj,props,val){
  if (props.length){
    obj[props[0]] = props.length > 1 ? {} : val;
    arrayToStructuredObject(obj[props.shift()],props,val);
  }
  return obj;
}

Though I loved the method of splitting the camelCase props by a look-ahead (/?=[A-Z]/) it takes an extra job of lower casing the whole array of prop strings regardless they are already lowercase or not. So i guess this might be slightly faster. (..or not due to the recursive nature of it)

Redu
  • 25,060
  • 6
  • 56
  • 76
  • I my real world case I am using AngularJS which converts hyphenated attributes to camelCase – Luke T O'Brien Apr 03 '16 at 09:36
  • @Luke T O'Brien Well in case you must definitely deal with camelCase props then above i have modified my solution into a complete one. – Redu Apr 03 '16 at 12:36
0

This is not the best solution, but you can actually use arrays as key, in this particular situation, by converting them to a string:

obj[["animation","Jump"].join()] = "123";

This will work with your original object.

Tim
  • 5,521
  • 8
  • 36
  • 69
0

A solution, which uses Regex to split camel case string.

var obj = { "animationsJump": "123", "animationsRun": "456", "animationsHide": "789", "personName": "Grant", "personPetsDog": "Snowy", "personPetsCat": "Snowball" },
    newObject = {};

Object.keys(obj).forEach(function (k) {
    var path = k.split(/(?=[A-Z])/).map(function (s) {
            return s.toLowerCase();
        }),
        last = path.pop();
    path.reduce(function (r, a) {
        r[a] = r[a] || {};
        return r[a];
    }, newObject)[last] = obj[k];
});

document.write('<pre>' + JSON.stringify(newObject, 0, 4) + '</pre>');
Community
  • 1
  • 1
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392