Nemo
Nemo

Reputation: 3164

Create an object out of dot notation

This is a reverse question to this question.

Given an object x={a:1,b:2} and a string c.d=3, modify object x to the following:

{
  a:1,
  b:2,
  c:{
    d:3
  }
}

I'm looking for a solution that does not use eval. The use case is as follows:

x being a configuration object, we call: config.set("music.shuffle",true)

Now, music.shuffle must be parsed somehow and added to the internal object x inside the config.set function, so that x looks something like:

x={a:1,b:2,music:{shuffle:true}}

Upvotes: 7

Views: 7316

Answers (7)

matsurena
matsurena

Reputation: 71

What about this :

function setObj (str, value, obj) {
    var ref = obj, keys = str.split('.');
    while (keys.length) {
        var currentKey = keys.shift();
        ref[currentKey] = keys.length ? (ref[currentKey]  ? ref[currentKey] : {}) : value;
        ref = ref[currentKey];
    }
}

Example with an input object using (could be some form values extracted with $.serializeArray)

var serializedInputs = [
    {name: 'fruits[1][name]', value: 'Banana'},
    {name: 'fruits[1][id]', value: '1'},
    {name: 'fruits[2][name]', value: 'Strawberry'},
    {name: 'fruits[2][id]', value: '2'},
    {name: 'fruits[3][name]', value: 'Raspberry'},
    {name: 'fruits[3][id]', value: '3'},
    {name: 'fruits[4][name]', value: 'Kiwi'},
    {name: 'fruits[4][id]', value: '4'},
    {name: 'fruits[5][name]', value: 'Mango'},
    {name: 'fruits[5][id]', value: '5'},
    {name: 'selected_fruit_id', value: '1'},
]
// This variable holds the result
var obj = {}
serializedInputs.forEach(function(item) {
    // Turning square brackets into dot notation
    setObj(item.name.replace(/\]/g, '').replace(/\[/g, '.'), item.value, obj);
})

Result

{
    "fruits": {
        "1": {
            "name": "Banana",
            "id": "1"
        },
        "2": {
            "name": "Strawberry",
            "id": "2"
        },
        "3": {
            "name": "Raspberry",
            "id": "3"
        },
        "4": {
            "name": "Kiwi",
            "id": "4"
        },
        "5": {
            "name": "Mango",
            "id": "5"
        }
    },
    "selected_fruit_id": "1"
}

Upvotes: 0

Steven de Salas
Steven de Salas

Reputation: 21497

How about this?

It'll create or copy/overwrite an existing object.

function expando(obj, base) {
    return Object.keys(obj)
      .reduce((clone, key) => {
        key.split('.').reduce((innerObj, innerKey, i, arr) => 
          innerObj[innerKey] = (i+1 === arr.length) ? obj[key] : innerObj[innerKey] || {}, clone)
        return clone;
    }, Object.assign({}, base));
}

console.log(expando({'a.b': 1})) // { a: { b : 1 }}
console.log(expando({'b.c': 2}, { a: 1 })) // { a: 1, b: { c: 2 }}
console.log(expando({'b.c.d': 2, 'e.f': 3}, { a: 1 })) // { a: 1, b: { c: { d: 2 } }, e: { f: 3}}

NOTE: ES6 needed for arrow functions and Object.assign().

Feel free to play around:

https://jsbin.com/setazahuce/1/edit?js,console

Upvotes: 1

krasnaya
krasnaya

Reputation: 3135

You can do this with lodash.set()

> l=require('lodash')
> x={a:1,b:2};
{ a: 1, b: 2 }
> l.set(x, 'c.d', 3)
{ a: 1, b: 2, c: { d: 3 } }

Upvotes: 7

Kevin Jhangiani
Kevin Jhangiani

Reputation: 1607

Had to do something similar today, here's another solution. Definitely could use some cleanup, but it does the trick. This will extend an existing object and won't wipe any data provided the input is valid.

There's no validation, so you can definitely overwrite keys if you pass it bad data.

// @param object orig    the object to extend
// @param array keyParts the.key.path split by "." (expects an array, presplit)
// @param mixed value    the value to assign
// @param object scoped  used by the recursion, ignore or pass null
function unflatten(orig, keyParts, value, scoped) {
    if (!scoped) {
        scoped = orig;
    }

    var nextKey = keyParts.shift();

    if (keyParts.length === 0) {
        scoped[nextKey] = value;
        return orig;
    }

    if (!scoped[nextKey]) {
        scoped[nextKey] = {};
    }

    scoped = scoped[nextKey];
    return unflatten(orig, keyParts, value, scoped);
}

The function prototype can be improved, but serves my needs. Call it via:

var orig = { foo: 'hello world', bar: { baz: 'goodbye world' } };

// lets add the key "bar.cat.aww" with value "meow"
unflatten(orig, "bar.cat.aww".split("."), "meow");
/* 
  orig is { 
    foo: "hello world", 
    bar: { 
      baz: "goodbye world", 
      cat: { 
        aww: "meow" 
      } 
    } 
  }

*/

// we can keep calling it to add more keys
unflatten(orig, "some.nested.key".split("."), "weeee");

/* 
  orig is { 
    foo: "hello world", 
    bar: { 
      baz: "goodbye world", 
      cat: { 
        aww: "meow" 
      } 
    },
    some: {
      nested: {
        key: "weeee"
      }
    }
  }
*/

Upvotes: 2

Alex Wayne
Alex Wayne

Reputation: 187144

Here's a heavily commented version that should be somewhat straightforward to understand.

// stores the configured data
configStore = {};

config = {
  set: function(keyValueString) {

    // Split the string by the =
    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; // used in loop

    // the current level of object we are drilling into.
    // Starts as the main root config object.
    var currentObj = configStore;

    // Loop through all keys in the key path, except the last one (note the -1).
    // 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] = {};
      }

      // Set the current object to the next level of the keypath,
      // allowing us to drill in.
      currentObj = currentObj[key];
    }

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

    // Set the property of the deepest object to the value.
    currentObj[lastKey] = value;
  }
};

// Do it.
config.set('omg.wtf.bbq=123')

// Check it.
alert(configStore.omg.wtf.bbq); // 123

Upvotes: 2

nnnnnn
nnnnnn

Reputation: 150080

Off the top of my head I guess you can do something like this:

function addValueToObj(obj, newProp) {
    newProp = newProp.split("=");       // separate the "path" from the "value"

    var path = newProp[0].split("."),     // separate each step in the "path"
        val = newProp.slice(1).join("="); // allow for "=" in "value"

    for (var i = 0, tmp = obj; i < path.length - 1; i++) {
       tmp = tmp[path[i]] = {};     // loop through each part of the path adding to obj
    }
    tmp[path[i]] = val;             // at the end of the chain add the value in
}

var x = {a:1, b:2};
addValueToObj(x, "c.d=3");
// x is now {"a":1,"b":2,"c":{"d":"3"}}
addValueToObj(x, "e.f.g.h=9=9");
// x is now {"a":1,"b":2,"c":{"d":"3"},"e":{"f":{"g":{"h":"9=9"}}}}

Demo: http://jsfiddle.net/E8dMF/1/

Upvotes: 11

Frances McMullin
Frances McMullin

Reputation: 5706

I believe dojo's setObject does what you want. If you (understandably) don't want to pull in all of dojo then I would recommend examining their (freely available) source or loading just base (only 4k) via AMD. It looks something like this :

function setObject(name, value, context) {
    var parts=name.split("."), 
    p=parts.pop();
    for(var i=0, j; context && (j=parts[i]); i++){
        context = (j in context ? context[j] : context[j]={});
    }
    return context && p ? (context[p]=value) : undefined; // Object
}

So in your case you would do :

x={a:1,b:2};
setObject("c.d", 3, x);

Warning : unless you only ever deal with trivial cases, I would urge you to still go check out the full dojo implementation, which deals with cases where no context is provided etc.

Upvotes: 4

Related Questions