11

I have created an AngularJS directive with radio buttons to control which environment my page will query against and added it to my page. I am using a two-way binding to map a local scope variable called environment to an app variable with the same name. It seems to work well when I create the radio buttons explicitly in the template (in my actual code I'm using templateUrl instead, but still have the same problem).

<div>
    <label><input type="radio" name="env" ng-model="environment" ng-change="changeEnvironment(1)" value="1" />Testing</label>
    <label><input type="radio" name="env" ng-model="environment" ng-change="changeEnvironment(2)" value="2" />Production</label>
</div>

I can select each choice as many times as I want, and the value bubbles all the way up to the app's scope variable.

I wanted to change the creation of the radio buttons to use ng-repeat on an array of choice objects. It creates the options, and it captures the ngChange event, but only once for each choice.

<label ng-repeat="choice in choices">
    <input type="radio" name="env" ng-model="environment" ng-change="changeEnvironment(choice)" value="{{ choice.id }}" />{{ choice.name }}
</label>

Here is the working version fiddle and the relevant parts of my directive code:

template: '<div>'
            + '<label><input type="radio" name="env" ng-model="environment" ng-change="changeEnvironment(1)" value="1" />Testing</label>'
            + '<label><input type="radio" name="env" ng-model="environment" ng-change="changeEnvironment(2)" value="2" />Production</label>'
            + '<div>Directive environment: {{ environment }}</div>'
            + '</div>',
link: function(scope, element, attributes) {

    scope.changeEnvironment = function(choiceID) {
        console.log('SELECTED environment ' + choiceID);
        scope.environment = choiceID;
        console.log('directive environment = ' + scope.environment);
    };

}

And here is the version that only works once:

template: '<div><label ng-repeat="choice in choices">'
            + '<input type="radio" name="env" ng-model="environment" ng-change="changeEnvironment(choice)" value="{{ choice.id }}" />{{ choice.name }}'
            + '</label>'
            + '<div>Directive environment: {{ environment }}</div>'
            + '</div>',
link: function(scope, element, attributes) {
    scope.choices = [
        { id: 1, name: "Testing" },
        { id: 2, name: "Production" }
    ];

    scope.changeEnvironment = function(choice) {
        console.log('SELECTED environment ' + choice.id);
        scope.environment = choice.id;
        console.log('directive environment = ' + scope.environment);
    };

}

I'm brand-new to AngularJS, so it's entirely possible I'm making a very basic mistake. Can anyone point me in the right direction?

UPDATE As per callmekatootie's suggestion, I changed the event in question from ng-change to ng-click, and it fires every time. That will do as a work-around for now, but I originally used ng-change because I didn't think ng-click would apply to changes caused by clicking on the text label rather than the input itself, but in fact it does. Still don't get why ng-change only fires once, though.

Danny
  • 1,740
  • 3
  • 22
  • 32
  • 1
    Try using `ng-click` instead of `ng-change` - an alternative though. I have no idea why `ng-change` does not fire up for each change. – callmekatootie May 21 '14 at 19:14
  • @callmekatootie - That seems to fix it. I originally had it as `ng-change` because I didn't think the `ng-click` would apply to the label, so I wanted to capture the event *however* it occurred. The odd thing is that clicking on the text also seems to fire `ngClick` on the input itself - I didn't expect that. This'll help me move forward, although I'm still not sure as to *why* it only works once with `ng-change`. – Danny May 21 '14 at 19:28
  • It's due to how prototypal inheritance works in JavaScript combined with the fact that each element in ng-repeat creates a new scope. I can explain in an answer if you still need it. – tasseKATT May 21 '14 at 19:31
  • @tasseKATT - that would be great, if you don't mind. It does seem like it's setting its state the first time it is clicked, and its not being updated when the other one gets selected. It seems related to the fact that if I remove the `name` attribute from the radio buttons, they stop working in tandem. I don't think they're as tied to the `environment` variable as I thought they were. – Danny May 21 '14 at 19:39

3 Answers3

8

The cause of the problem is that ngRepeat will create new scopes for its children (along with how prototypal inheritance affects scopes).

The Angular wiki has a fairly good (and thorough) explanation of how scope inheritance works (and the common pitfalls).

This answer to a very similar (in nature) problem pretty much explains the issue.

In your specific case, you could introduse an object (e.g. local) and assign environment as its property):

scope.local = {environment: scope.environment};
scope.changeEnvironment = function(choice) {
    scope.environment = choice.id;
};

Then you should bind your template to that "encaplulated" property:

<input ... ng-model="local.environement" ... />

See, also, this short demo.


UPDATE

This answer intends to point out the root of the issue.
A different implementation (like the ones proposed by tasseKATT or Leon) is definitely recommended (and way more "Angularish").

Community
  • 1
  • 1
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • Since I'm learning, I'm happy to get to the *why* of it - thanks for your help. I must confess I am little unsure of which of the rather different answers is the "best". DML's is definitely the easiest (and worked). – Danny May 21 '14 at 19:58
  • 1
    I can't decide myself either :) I usually don't...I actually never use `$parent` (for several different reasons), but I really can't think of anything wrong in using it as Leon suggests. On the other hand, if you can live with having to convert `environment` to an object in the controller scope, then tasseKATT's solution is more straight-forward. – gkalpak May 21 '14 at 20:02
4

Use an object instead of a primitive value:

app.controller('mainController', ['$scope', function (scope) {
  scope.environment = { value: '' };
}]);

Remove ng-change and $watch. ng-model will be enough (unless I'm misunderstanding the use case).

Demo: http://jsfiddle.net/GLAQ8/

Very good post on prototypal inheritance can be found here.

Community
  • 1
  • 1
tasseKATT
  • 38,470
  • 8
  • 84
  • 65
  • I was using `$watch` because I kick off a service to pull data from the server any time the user changes the environment. I was writing out the values of `environment` in various places just to see how/when they change. Does that sound like a reasonable use of `$watch`? – Danny May 21 '14 at 19:56
  • Yes, both ng-change and $watch can be used for that scenario. – tasseKATT May 21 '14 at 19:58
  • It seems like both this and the answer with `$parent` work, but I've implemented it this way because I think it aligns more closely with how I was originally picturing the two-way binding working. `$parent` feels a little like "yeah, this is your local variable, but..." – Danny May 21 '14 at 20:33
  • I tried with your solution but it is not working for me ,I am having ng-repeat inside ng-repeat . – Anuj Feb 06 '17 at 12:54
2

you are using enviorement instead of $parent.enviorement in your ng-change event which is tied to the repeat scope not the the ng-repeat parent scope where the enviorement variable lives,

http://jsfiddle.net/cJ4Wb/7/

ng-model="$parent.enviroment"

notice that in this case you don't even need the events to keep enviorement updated and any changes in the model will refelct in the radio buttons

Dayan Moreno Leon
  • 5,357
  • 2
  • 22
  • 24
  • This got `ng-change` working - thanks. So in this case `$parent` refers to the directive's `environment`, and not the variable in the same name in the app, right? It's just bubbling to the top because of the two-way binding, right? – Danny May 21 '14 at 19:53
  • 1
    this referes to the enviroment variable tha lives in the scope of your directive which happes to be the same variable in the controller since you tide it using ' environment: '='' and whit this you can just remove the ng-change as i demostrate in the fiddle – Dayan Moreno Leon May 21 '14 at 20:01