1

I'm currently struggling with a JavaScript problem. I want to return a multi-level property, as well as every variable contained within, by passing in the original object, and an array of paths to the properties I want.

For example, if I have the following object:

obj = {
  product: {
    candidate: {
      id: 10,
      reference: "test",
      count: 4,
      steps: 10
    }
  }
}

I want to be able to call a method:

getVarPath(obj, ["product.candidate.ID", "product.candidate.reference"])

And then have it return one object with each variable passed in the array, in it's original structure. So this would return an object looking like so:

{
  product: {
    candidate: {
      id: 10,
      reference: "test"
    }
  }
}

I do have this working in my local solution at the moment (passing in one string rather than an array at the moment).

The solution at the moment is pretty horrid but I'm looking to improve it, so if anyone can think of a better method that would be great. Again, this is pretty horrid right now but I'm looking to improve it. But it does the job:

var getVarPath = function(obj, keys){
  var elements = keys.split("."),
      evalStr = "",
      objStr = "obj",
      newObjStr = "newObj",
      newObj = {};

  if(elements.length > 1){

    elements.forEach(function(key, index){

      // first append a property accessor at the end of the eval string
      evalStr = evalStr + "['" + key + "']";

      // if we're at the last element, we've reached the value, so assign it
      if(index === elements.length -1){

        eval(newObjStr + evalStr + " = " + objStr + evalStr);
      }
      else {
        // if we're not at the last, we're at an object level
        // if the nested object doesn't exist yet, create it
        if(!eval(newObjStr + evalStr)){
          eval(newObjStr + evalStr + " = {};");
        }
      }
    });

  }

  return newObj;
}
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
TomDavies
  • 29
  • 4
  • I've edited the question with my current working solution but I don't like the fact I'm using eval, looking to make it a bit nicer – TomDavies Aug 02 '17 at 08:39

3 Answers3

0

For each element in the input array:

First, you can split the initial string: var nestedElements="product.candidate.ID".split(.)"
This returns an array with each level: ["product","candidate","ID"]

Now you can access to your nested object using each element of the array: obj["product"]["candidate"]["ID"] either by using a loop over the array or recursion.

var currentobj=obj;
for (var i=0;i<nestedElements.length;i++){
  currentobj=currentobj[nestedElements[i]]
}
// currentobj is your id      

In the same process, you could dynamically add elements to a new obj using a similar process:

newobj={} //before loop
if (newobj["product"] === undefined) newobj["product"]={} //in loop

And that should be done for each element on the input array, in the end is iterating through arrays and accessing the object using strings

angrykoala
  • 3,774
  • 6
  • 30
  • 55
  • Hey, thanks for your answer! I tried implementing your example here but it doesn't seem to work for me, I get exceptions thrown when it tries to go deeper than one element, did you get this working yourself? – TomDavies Aug 02 '17 at 08:41
  • This example is mostly pseudocode with the basic logic, not actual working code, you'll need to adapt it. What exceptions are you getting? – angrykoala Aug 02 '17 at 10:43
0

Your code as-is shouldn't actually work. You're treating keys as a string, but passing in an array. You can (and should) avoid using eval(), by keeping track of the "inner" objects you're currently looking at, and using object[property] notation instead of object.property.

function getVarPath(obj, keys) {
  var result = {};

  // ["product.candidate.id", "product.candidate.reference"]
  keys.forEach(function(key) {
    var src = obj,     // inner source object
        dest = result, // inner destination object
        parts = key.split(/\./);

    // e.g. ["product", "candidate", "id"]
    parts.forEach(function(part) {

      // if we're looking at an object, make sure it exists in the dest
      if (typeof(src[part]) === "object")
        dest[part] = dest[part] || {};
      // if it's just a value, copy it  
      else
        dest[part] = src[part];

      dest = dest[part]; // move from obj to obj.product, then to obj.product.candidate, etc.
      src = src[part];
    });
  });

  return result;
}

var obj = {
  product: {
    candidate: {
      id: 10,
      reference: "test",
      count: 4,
      steps: 10
    }
  }
}

var output = getVarPath(obj, ["product.candidate.id", "product.candidate.reference"]);

console.log(JSON.stringify(output));
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
0

Using _.propertyOf(), Array#reduce(), and Object.assign(), as well as computed property names, you could create a less daunting implementation:

function getVarPath(object, paths) {
  return paths.reduce(function (accumulator, path) {
    const that = _.propertyOf(accumulator)
    let walk = path.split('.')
    let value = this(walk)

    for (let key = walk.pop(); key !== undefined; key = walk.pop()) {
      const base = that(walk)

      value = { [key]: value }

      if (base !== undefined) {
        value = Object.assign(base, value)
      }
    }

    return Object.assign(accumulator, value)
  }.bind(_.propertyOf(object)), {})
}

let obj = {
  product: {
    candidate: {
      id: 10,
      reference: "test",
      count: 4,
      steps: 10
    }
  }
}

console.log(getVarPath(obj, ['product.candidate.id', 'product.candidate.reference']))
<script src="https://cdn.rawgit.com/lodash/lodash/4.17.4/dist/lodash.min.js"></script>
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153