Сравнение равенства объектов для ввода [радио] с ng-моделью и ng-значением

Позвольте мне начать с того, что этот вопрос очень похож на проблемы с выбором в теге <select>, используя ng-options. Например, Работа с выбором с помощью ng-опций AngularJS. Конкретной проблемой является сравнение двух разных экземпляров объекта, которые не являются ссылочными, но которые логически представляют одни и те же данные.

Чтобы продемонстрировать, скажем, у нас есть следующий массив параметров и выбранная переменная опции в модели:

$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'};

Обратите внимание, что указанные объекты предназначены только для демонстрации. Я специально отказался от свойства "Extra" на selectedItem, чтобы показать, что иногда мои объекты модели отличаются своими специфическими свойствами. Важно то, что я хочу сравнить свойство ID. У меня есть функция equals() на моих реальных объектах, которая сравнивает как прототип 'class', так и ID.

И затем в представлении:

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

Теперь проблема заключается в том, что переключатель "Foo" не будет запущен, потому что angular использует ссылочное равенство для объектов. Если бы я изменил последнюю строку в своей области на ниже, все будет работать так, как ожидалось.

$scope.selectedItem = items[0];

Но проблема у меня в том, что в моем приложении я не просто объявляю эти две простые переменные в области видимости. Скорее, список опций и структура данных, в которых привязан выбранный параметр, являются частью более крупных наборов данных JSON, которые запрашиваются с сервера с помощью $http. В общем случае для меня очень сложно перейти на выбранное свойство привязки данных к эквивалентной опции из моего запроса данных.

Итак, мой вопрос: В ng-опциях для <select>, angular предлагает выражение track by, которое позволяет мне сказать что-то вроде "object.ID" и сообщить angular, что он должен сравнивать выбранное значение модели с параметрами с помощью идентификатора имущество. Есть ли что-то подобное, что я могу использовать для множества радиовходов, связанных с одним и тем же свойством модели? В идеале я мог бы сказать angular использовать мой собственный метод equals(), который я разместил на этих объектах модели, который проверяет как тип объекта, так и идентификатор. Если это не удастся, возможно, также будет иметь возможность указывать сопоставление идентификаторов.

Ответ 1

Я пишу самую простую директиву. Использование своего рода треков для сопоставления двух разных объектов. См. 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;
            }
        }
    };
});

Ответ 2

ОК, поэтому после дальнейшего рассмотрения я решил пойти с более "смешанным" подходом, просто заменив директиву ng-model своей собственной настраиваемой директивой. Это очень похоже на подход, который я использовал для создания директивы "checkbox list" на основе этого ответа: fooobar.com/questions/13389/....

.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 );
            });
        }
    };
});

Ответ 3

Как запрошено OP, здесь приведен пример директива переключателя, которая будет работать со сложными объектами. Он использует underscore.js, чтобы найти выбранный элемент из параметров. Это немного сложнее, чем должно быть, потому что оно также поддерживает загрузку параметров и выбранного значения с помощью вызовов AJAX.

Ответ 4

Почему бы вам просто не использовать идентификатор для этого выбора?

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

А затем вместо selectedItem вы можете написать items[selectedItem].

О, и, играя с вашей проблемой в jsfiddle, я заметил другие вещи:

a.) Вы забыли добавить к вводу атрибут name.

b.) Никогда не используйте что-либо без точки в ng-модели. Если вы действительно пытаетесь вывести selectedItem с {{selectedItem}} вне блока ng-repeat, вы заметите, что значение не обновляется при выборе переключателя. Это связано с тем, что ng-repeat создает собственный охват пользователя.

Ответ 5

Так как я еще не могу добавлять комментарии, я должен ответить здесь. Ответ Даны работал нормально для меня. Хотя я хотел бы указать на то, чтобы использовать его подход, нужно было бы реализовать функцию "равно" на объектах коллекции. Пример ниже:

.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'} };

 }]);

Смотрите ссылку plunker .