Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS: nested objects from dynamically set model names

I have an array containing variable names, example:

var names = ['address.street','address.city'];

I want to create input fields out of these, and I'm using AngularJS. No big deal:

<div ng-repeat="n in names">
    <input type="text" ng-model="data[n]" />
</div>

The resulting $scope.data object is:

{
    "address.street" : ...,
    "address.city" : ...
}

Which, by the way, is not exactly what I'm trying to achieve. Is there a syntax that could lead me to an object as the following one as result?

{
    "address" : {
        "street" : ...,
        "city" : ...
    }
}

Please consider that I can have even more than one level of nesting, this is just an example.

like image 832
Lorenzo Marcon Avatar asked Nov 21 '13 11:11

Lorenzo Marcon


3 Answers

I do not think models should be accessed this way.

However, this was curious question and the solution is a bit fun.

The problem is that ng-model requires a reference and thought Javascript sends modifiable copies of objects, it does not have pass-by-reference semantics and we cannot just pass a string to ng-model.

However, arrays and objects do have this property. Hence, the solution is to return an array whose 0th element will be the reference for ng-model. This is also the hacky part since all your objects are now arrays with '1' element.

The other solution would be to return an object for each case instead of 1 element array.

Solution using embedded objects

Here is the solution using an embedded object: http://plnkr.co/edit/MuC4LE2YG31RdU6J6FaD?p=preview which in my opinion looks nicer.

Hence, in your controller:

$scope.getModel = function(path) {
  var segs = path.split('.');
  var root = $scope.data;

  while (segs.length > 0) {
    var pathStep = segs.shift();
    if (typeof root[pathStep] === 'undefined') {
      root[pathStep] = segs.length === 0 ? { value:  '' } : {};
    }
    root = root[pathStep];
  }
  return root;
}

And in your template:

<p>Hello {{data.person.name.value}}!</p>
<p>Address: {{data.address.value}}</p>
<input ng-model="getModel('person.name').value" />
<input ng-model="getModel('address').value" />

Solution using single element array

Here is the shortest (albeit hacky) solution I could come up with: http://plnkr.co/edit/W92cHU6SQobot8xuElcG?p=preview

Hence, in your controller:

$scope.getModel = function(path) {
  var segs = path.split('.');
  var root = $scope.data;

  while (segs.length > 0) {
    var pathStep = segs.shift();
    if (typeof root[pathStep] === 'undefined') {
      root[pathStep] = segs.length === 0 ? [ '' ] : {};
    }
    root = root[pathStep];
  }
  return root;
}

And in your template:

<p>Hello {{data.person.name[0]}}!</p>
<p>Address: {{data.address[0]}}</p>
<input ng-model="getModel('person.name')[0]" />
<input ng-model="getModel('address')[0]" />
like image 53
musically_ut Avatar answered Nov 14 '22 05:11

musically_ut


The answer provided by @musically_ut is good but has one significant flaw: It will work great if you're creating a new model but if you have an pre-defined existing model that you can't refactor into the '.value' structure or the array structure, then you're stuck...

Clearly that was the case for me... (and I assume that was the case for @LorenzoMarcon too, as he's implying that he'll have to "post-process" the result and transform it to a different format)

I ended up elaborating on @musically_ut's solution:

    $scope.getModelParent = function(path) {
      var segs = path.split('.');
      var root = $scope.data;

      while (segs.length > 1) {
        var pathStep = segs.shift();
        if (typeof root[pathStep] === 'undefined') {
          root[pathStep] = {};
        }
        root = root[pathStep];
      }
      return root;
    };

    $scope.getModelLeaf = function(path) {
      var segs = path.split('.');
      return segs[segs.length-1];
    };

(note the change in the while loop index)

Later on you access the dynamic field like this:

<input ng-model="getModelParent(fieldPath)[ getModelLeaf(fieldPath) ]"/>

The idea is (as explained in @musically_ut's answer) that JS can't pass a string by reference, so the hack around it I pass the parent node (hence the while loop inside 'getModelParent' stops before the last index) and access the leaf node (from 'getModelLeaf') using an array like notation.

Hope this makes sense and helps.

like image 33
roy650 Avatar answered Nov 14 '22 07:11

roy650


If you can restructure your models, you can simply do like this:

Controller

$scope.names = {
    "address":[
        "street",
        "city"
    ]
};

$scope.data = {
    address:{
        street:"",
        city:""
    }
};

HTML

<div ng-repeat="(key, values) in names">
    <div ng-repeat="value in values">
        <input type="text" ng-model="data[key][value]" />
    </div>
</div>
like image 33
AlwaysALearner Avatar answered Nov 14 '22 07:11

AlwaysALearner