11

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.

2
  • Do you mean your $scope.data is { "address" : { "street" : ..., "city" : ... } }. Its not clear Commented Nov 21, 2013 at 11:52
  • I'm trying to make it like you wrote Commented Nov 21, 2013 at 11:58

4 Answers 4

9

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]" />
Sign up to request clarification or add additional context in comments.

2 Comments

I see the effort you made, and thanks for the detailed explanation. This is close, but the output data object would be different than the format I'm looking for (you can see it in my question). Given the restrictions I have, maybe yours is the only viable way, in this case I probably have to post-process again the resulting object to transform it in the format I need.
@LorenzoMarcon Thanks for accepting the answer. I do realise that this is not the solution you wanted, but this was an interesting problem to think about anyway. You have my (+1) for the fun. :)
8

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.

1 Comment

Thanks roy650.This works for me as in musically_ut solution u will be getting "address:{city:{value:{}", but with the above, u won't get the extra "value:{}". u can also do like $scope.getModelLeaf = function(path) { var segs = path.split('.'); return segs.pop(); };
1

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>

4 Comments

From the question: "Please consider that I can have even more than one level of nesting, this is just an example."
We would have nested ng-repeats in that case.
Indeed, there are better ways of approaching the problem, but this problem itself is a curious one. :-)
Thanks for the answer, I was looking for a way to do this without changing the initial model array. Furthermore, some elements could have no nesting (e.g. name) some other may have one or more (e.g. address.street), but I don't know it in advance, so this solution is quite limited for my case
0

For easier parsing of paths you can also check lodash's methods :

_.get($scope, 'model.nested.property', 'default');   
_.set($scope, 'model.nested.property', 'default');   
_.has($scope, 'model.nested.property');

https://lodash.com/docs#get

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.