15

Let me start by saying that this question is very similar to issues with selection in a <select> tag using ng-options. For example, Working with select using AngularJS's ng-options. The specific problem is comparing two different instances of an object which are not reference equal, but which logically represent the same data.

To demonstrate, let's say we have the following array of options and selected option variable in the model:

$scope.items = [
   {ID: 1, Label: 'Foo', Extra: 17},
   {ID: 2, Label: 'Bar', Extra: 18},
   {ID: 3, Label: 'Baz', Extra: 19}
];
$scope.selectedItem = {ID: 1, Label: 'Foo'};

Note that the above objects are just for demonstration. I specifically left off the 'Extra' property on selectedItem to show that sometimes my model objects differ in their specific properties. The important thing is that I want to compare on the ID property. I have an equals() function on my real objects that compares both prototype 'class' and ID.

And then in the view:

<label class="radio inline" ng-repeat="item in items">
    <input type="radio" ng-model="selectedItem" ng-value="item"> {{item.Label}}
</label>

Now, the problem here is that the radio button for 'Foo' will not start selected, because angular is using reference equality for the objects. If I changed the last line in my scope to the below, everything would work as expected.

$scope.selectedItem = items[0];

But, the problem I'm having is that in my application, I'm not simply declaring these two simple variables in scope. Rather, the options list and the data structure where the selected option are being bound are both part of larger sets of JSON data that are queried from the server using $http. In the general case, it's very difficult for me to go change the data-bound selected property to be the equivalent option from my data query.

So, my question: In ng-options for the <select>, angular offers a track by expression that allows me to say something like "object.ID" and inform angular that it should compare the selected model value to the options via the ID property. Is there something similar that I can use for a bunch of radio inputs all bound to the same model property? Ideally, I would be able to tell angular to use my own custom equals() method that I've placed on these model objects, which checks both object type as well as ID. Failing that though, being able to specify ID comparison would also work.

Community
  • 1
  • 1
Dana Cartwright
  • 1,566
  • 17
  • 26
  • We solved this with [findWhere](http://underscorejs.org/#findWhere) method from underscore.js by selecting the value from options at start. I would also like to know if it can be done that way. – Ufuk Hacıoğulları Oct 09 '13 at 20:05
  • I'm not using undescore.js, but I don't mind rolling my own comparison function to do what I need. Where did you actually employ findWhere? In a custom directive? – Dana Cartwright Oct 09 '13 at 20:09
  • Yes, we had radio button, dropdown directives just for this. – Ufuk Hacıoğulları Oct 09 '13 at 20:19
  • I would be curious to know how you structured the directives in your solution. That might actually be my answer. – Dana Cartwright Oct 09 '13 at 20:39
  • 2
    Here is a [radio button directive](http://plnkr.co/edit/E3c6uwMnqIckTJmqAylO?p=preview). It's a little complex because it still works even if you load the options with an AJAX call. You may want to make it simpler. – Ufuk Hacıoğulları Oct 09 '13 at 21:24
  • I think your radio button directive is the answer I'm looking for. I am going to go ahead with implementing a directive along the same lines that suits my particular situation, but why don't you go ahead and write up your comment as an answer so I can give you credit on SO. – Dana Cartwright Oct 10 '13 at 12:22
  • I just ran into this problem... If you take your example, even if you had `$scope.selectedItem = {ID: 1, Label: 'Foo', Extra: 17};` where all properties are included and equal, it still doesn't become selected. I thought this case would've been covered but it seems not :( – David Spence Jul 17 '14 at 14:28

5 Answers5

10

I write a most simple directive. Using a kind of "track-by" to map two different objects. See the http://jsfiddle.net/xWWwT/146/.

HTML

<div ng-app="app">
<div ng-app ng-controller="ThingControl">    
    <ul >
        <li ng-repeat="color in colors">
            <input type="radio" name="color" ng-model="$parent.thing" ng-value="color" radio-track-by="name" />{{ color.name }}
        </li>
    </ul>
    Preview: {{ thing }}
</div>
</div>

JS

var app = angular.module('app', []);

app.controller('ThingControl', function($scope){
    $scope.colors = [
        { name: "White", hex: "#ffffff"},
        { name: "Black", hex: "#000000"},
        { name: "Red", hex: "#000000"},
        { name: "Green", hex: "#000000"}
    ];

    $scope.thing = { name: "White", hex: "#ffffff"};

});

app.directive('radioTrackBy', function(){
return {
        restrict: "A",
        scope: {
            ngModel: "=",
            ngValue: "=",
            radioTrackBy: "@"
        },
        link: function (ng) {   
            if (ng.ngValue[ng.radioTrackBy] === ng.ngModel[ng.radioTrackBy]) {                                
                ng.ngModel = ng.ngValue;
            }
        }
    };
});
  • 1
    With a few small modifications, I got this to work for `mdSelect` elements too. https://gist.github.com/jonahbron/7a1f0c5bff949e3765b6 – Jonah Oct 20 '15 at 19:16
  • 1
    I have it working with complex model after adding `require: 'ngModel'` to the directive – César Jun 10 '16 at 00:19
2

OK, so after further review, I decided to go with a more "mix-in" approach, just replacing the ng-model directive with my own custom directive, in essence. This is very similar to the approach I used for making a "checkbox list" directive based on this answer: https://stackoverflow.com/a/14519881/561604.

.directive('radioOptions', function() {
    // Apply this directive as an attribute to multiple radio inputs. The value of the attribute
    // should be the scope variable/expression which contains the available options for the
    // radio list. Typically, this will be the collection variable in an ng-repeat directive
    // that templates the individual radio inputs which have radio-options applied. In addition,
    // instead of the normal ng-model, use a selected-option attribute set to the same expression.
    // For example, you might use radio-options like this:
    // <label ... ng-repeat="item in collection">
    //     <input type="radio" ... ng-value="item" radio-options="collection" selected-option="myModel.myProperty">
    // </label>
    //
    // See https://stackoverflow.com/questions/19281404/object-equality-comparison-for-inputradio-with-ng-model-and-ng-value
    // for the SO question that inspired this directive.
    return {
        scope: {
            radioOptions: '=',
            selectedOption: '=',
            ngValue: '='
        },
        link: function( scope, elem, attrs ) {
            var modelChanged =  function() {
                if( jQuery.isArray(scope.radioOptions) ) {
                    jQuery.each( scope.radioOptions, function(idx, item) {
                        // This uses our models' custom 'equals' function for comparison, but another application could use
                        // ID propeties, etc.
                        if( typeof item.equals === 'function' && item.equals(scope.selectedOption) ) {
                            elem.prop( 'checked', item === scope.ngValue );
                        }
                    });
                }
            };
            scope.$watch( 'radioOptions', modelChanged );
            scope.$watch( 'selectedOption', modelChanged );
            var viewChanged = function() {
                var checked = elem.prop( 'checked' );
                if( checked ) {
                    scope.selectedOption = scope.ngValue;
                }
            };
            elem.bind( 'change', function() {
                scope.$apply( viewChanged );
            });
        }
    };
});
Community
  • 1
  • 1
Dana Cartwright
  • 1,566
  • 17
  • 26
1

Why don't you just use the ID for the select like this?

<input type="radio" ng-model="selectedItem" ng-value="item.ID"> {{item.Label}}

And then instead of using selectedItem you could write items[selectedItem].

Oh, and while playing with your problem in jsfiddle I noticed to other things:

a.) You forgot to add a name attribute to the input.

b.) Don't ever use something without a dot in ng-model. If you actually try to output selectedItem with {{selectedItem}} outside the ng-repeat block, you will notice that the value does not update when you chose a radio button. This is due to ng-repeat creating a own child scope.

Luca
  • 9,259
  • 5
  • 46
  • 59
Juliane Holzt
  • 2,135
  • 15
  • 14
  • 1
    Yes, I see that would work in the limited example I posted. However, the problem I'm having is that in my actual application, I am indeed using a property of another object in ng-model. That property needs to be maintained with the actual object instance, not its ID. – Dana Cartwright Oct 09 '13 at 20:31
1

As OP requested, here's an example radio button directive that will work with complex objects. It uses underscore.js to find the the selected item from the options. It's a little more complicated than it should be because it also supports loading the options and selected value with AJAX calls.

Ufuk Hacıoğulları
  • 37,978
  • 12
  • 114
  • 156
0

Since I'm not yet able to add comments, so I have to reply here. Dana's answer worked ok for me. Although I'd like to point out in order to use his approach, one would have to implement the 'equals' function on the objects in the collection. See below example:

.controller('ExampleController', ['$scope', function($scope) {
   var eq = function(obj) {
       return this.id === obj.id;
     };
   col = [{id: 1, name: 'pizza', equals: eq}, {id:2, name:'unicorns', equals: eq}, {id:3, name:'robots', equals: eq}];

   $scope.collection = col;
   $scope.my = { favorite : {id:2, name:'unicorns'} };

 }]);

See the plunker link.

binjiezhao
  • 567
  • 8
  • 12
  • Yes, this is more or less how my code works. If all of your objects have an 'id' property though, it might be easier just to modify this line in the directive: `if( typeof item.equals === 'function' && item.equals(scope.selectedOption) ) {` To simply check: `if( item.id === scope.selectedOption.id ) {` But, you might have to do some null checks in there, not sure. – Dana Cartwright Dec 05 '14 at 17:00
  • I actually prefer the equals function as it gives calling client more flexibility as to how to do the comparisons. The application of command pattern springs to mind... – binjiezhao Dec 08 '14 at 15:36