Как реализовать ng-change для пользовательской директивы

У меня есть директива с шаблоном вроде

<div>
    <div ng-repeat="item in items" ng-click="updateModel(item)">
<div>

Моя директива объявлена ​​​​как:

return {
    templateUrl: '...',
    restrict: 'E',
    require: '^ngModel',
    scope: {
        items: '=',
        ngModel: '=',
        ngChange: '&'
    },
    link: function postLink(scope, element, attrs) 
    {
        scope.updateModel = function(item)
        {
             scope.ngModel = item;
             scope.ngChange();
        }
    }
}

Я хотел бы, чтобы ng-change вызывался при нажатии элемента, а значение foo уже было изменено.

То есть, если моя директива реализована как:

<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>

Я ожидаю, что вызову bar, когда значение foo будет обновлено.

С кодом, приведенным выше, ngChange вызывается успешно, но вызывается со старым значением foo вместо нового обновленного значения.

Один из способов решить проблему — вызвать ngChange внутри тайм-аута, чтобы выполнить его в какой-то момент в будущем, когда значение foo уже было изменено. Но это решение лишает меня контроля над порядком выполнения вещей, и я предполагаю, что должно быть более элегантное решение.

Я также мог бы использовать наблюдателя над foo в родительской области, но это решение на самом деле не дает реализации метода ngChange, и мне сказали, что наблюдатели являются большими потребителями памяти.

Есть ли способ заставить ngChange выполняться синхронно без тайм-аута или наблюдателя?

Пример: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview.


person htellez    schedule 15.07.2014    source источник
comment
Можете ли вы добавить скрипку или плунжер?   -  person Raghavendra    schedule 15.07.2014
comment
Вот, чувак: plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview   -  person htellez    schedule 15.07.2014
comment
Если я выполняю scope.$parent.$apply(); значение обновлено, но теперь генерируются исключения: errors.angularjs. org/undefined/$rootScope/inprog?p0=%24apply   -  person htellez    schedule 15.07.2014


Ответы (5)


Если вам требуется ngModel, вы можете просто вызвать $setViewValue для ngModelController, который неявно оценивает ng-change. Четвертым параметром функции связывания должен быть ngModelCtrl. Следующий код заставит ng-change работать для вашей директивы.

link : function(scope, element, attrs, ngModelCtrl){
    scope.updateModel = function(item) {
        ngModelCtrl.$setViewValue(item);
    }
}

Чтобы ваше решение работало, удалите ngChange и ngModel из изолированной области действия myDirective.

Вот пример: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview

person Samuli Ulmanen    schedule 22.09.2014
comment
Есть ли способ сделать это, не удаляя ngModel из изолированной области? Попытка реализовать двустороннюю привязку с помощью ngChange стала запутанной/неэффективной stackoverflow.com/questions/30575973/ - person JonoCoetzee; 02.06.2015
comment
Несмотря на решение проблемы, этот ответ скрывает реальную проблему. Пользовательская директива в вопросе не будет работать со многими распространенными директивами ng-*, потому что она не регистрирует, что значение представления было обновлено. Подробнее см. ответ @lucienBertin. - person Ed_; 11.03.2016
comment
Вы правы, Эд и @lucienBertin. Действительно, viewChangListeners не нужны. Установка значения представления через ngModelCtrl неявно оценивает выражение ng-change. Планк немного подчистил. Хороший. - person Samuli Ulmanen; 17.03.2016
comment
Это работает, но зависит от того, является ли ваш элемент типом значения. Если вы используете объекты, это будет работать только при первой установке ссылки. Вам нужно будет клонировать объект ref перед вызовом setViewValue, чтобы ngChange срабатывал в этих случаях (см. мой ответ ниже). - person Arkiliknam; 04.07.2016

тл;др

По моему опыту, вам просто нужно наследовать от ngModelCtrl. выражение ng-change будет автоматически оцениваться при использовании метода ngModelCtrl.$setViewValue

angular.module("myApp").directive("myDirective", function(){
  return {
    require:"^ngModel", // this is important, 
    scope:{
      ... // put the variables you need here but DO NOT have a variable named ngModel or ngChange 
    }, 
    link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
      scope.setValue = function(value){
        ctrl.$setViewValue(value); // this line will automatically eval your ng-change
      };
    }
  };
});

Точнее

ng-change оценивается во время ngModelCtrl.$commitViewValue() IF, когда ссылка на объект вашей ngModel изменилась. метод $commitViewValue() автоматически вызывается $setViewValue(value, trigger), если вы не используете аргумент триггера или не указали какие-либо ngModelOptions.

Я указал, что ng-change будет автоматически запускаться, если ссылка на $viewValue изменится. Если ваш ngModel — это string или int, вам не о чем беспокоиться. Если ваш ngModel является объектом и вы просто меняете некоторые его свойства, то $setViewValue не будет оцениваться ngChange.

Если взять пример кода из начала поста

scope.setValue = function(value){
    ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
    var vv = ctrl.$viewValue;
    vv.prop1 = prop1Value;
    ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
person lucienBertin    schedule 30.11.2015
comment
Отличное понимание. Читая ваше описание, я думаю, что мог бы вручную вызвать $commitViewValue() для принудительного ngChange (в случае обновления существующего объекта), но не так. Документация AngularJS предполагает, что пользовательские элементы управления также могут передавать объекты этому методу. В этом случае мы должны сделать копию объекта, прежде чем передавать его в $setViewValue. См. docs.angularjs.org/api/ng/type/ngModel.NgModelController - person Arkiliknam; 04.07.2016

После некоторых исследований кажется, что лучший подход — использовать $timeout(callback, 0).

Он автоматически запускает цикл $digest сразу после выполнения обратного вызова.

Итак, в моем случае решение заключалось в использовании

$timeout(scope.ngChange, 0);

Таким образом, не имеет значения, какова подпись вашего обратного вызова, он будет выполняться так, как вы определили его в родительской области.

Вот plunkr с такими изменениями: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview

person htellez    schedule 03.08.2014
comment
Это на самом деле отлично сработало для меня, так как у меня уже была функция $timeout, связанная с fn или обработкой текстовых полей. - person Scott Sword; 01.04.2015
comment
К вашему сведению, причина, по которой это работает, заключается в том, что $timeout автоматически заключает функцию в $scope.$apply, что приводит к запуску цикла $digest. Если вы этого не хотите, вы передаете в $timeout третий логический параметр, указывающий не делать этого. - person Gautham C.; 15.05.2015

Ответы Samuli Ulmanen и lucienBertin прибивают это, хотя небольшое дополнительное чтение в документации AngularJS дает дополнительные советы о том, как справиться с этим (см. https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).

В частности, в тех случаях, когда вы передаете объекты в $setViewValue(myObj). Документация AngularJS гласит:

При использовании со стандартными входными данными значение представления всегда будет строкой (которая в некоторых случаях преобразуется в другой тип, например объект Date для input[date].) Однако пользовательские элементы управления также могут передавать объекты этому методу. В этом случае мы должны сделать копию объекта, прежде чем передавать его в $setViewValue. Это связано с тем, что ngModel не выполняет глубокое наблюдение за объектами, а только ищет изменение личности. Если вы измените только свойство объекта, то ngModel не поймет, что объект изменился, и не будет вызывать пайплайны $parsers и $validators. По этой причине не следует изменять свойства копии после ее передачи в $setViewValue. В противном случае вы можете привести к неправильному изменению значения модели в осциллографе.

В моем конкретном случае моя модель представляет собой объект даты момента, поэтому я должен сначала клонировать объект, а затем вызывать setViewValue. Мне здесь повезло, так как момент предоставляет простой метод клонирования: var b = moment(a);

link : function(scope, elements, attrs, ctrl) {
    scope.updateModel = function (value) {
        if (ctrl.$viewValue == value) {
            var copyOfObject = moment(value);
            ctrl.$setViewValue(copyOfObject);
        }
        else
        {
            ctrl.$setViewValue(value);
        }
    };
}
person Arkiliknam    schedule 04.07.2016

Фундаментальная проблема здесь заключается в том, что базовая модель не обновляется до тех пор, пока цикл дайджеста, который происходит после завершения выполнения scope.updateModel. Если функции ngChange требуются сведения о выполняемом обновлении, то эти сведения можно явно сделать доступными для ngChange, а не полагаться на ранее примененное обновление модели.

Это можно сделать, предоставив карту имен локальных переменных для значений при вызове ngChange. В этом сценарии вы можете сопоставить новое значение модели с именем, на которое можно сослаться в выражении ng-change.

Например:

scope.updateModel = function(item)
{
    scope.ngModel = item;
    scope.ngChange({newValue: item});
}

В HTML:

<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>

См.: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview.

person chrisg    schedule 15.07.2014
comment
Настоящий ng-change никогда не должен знать аргументы. Для этого конкретного случая то, что вы предлагаете, будет решением, но что произойдет, если я попытаюсь использовать обратный вызов bar(a, b, c, ...) с двумя или более аргументами? - person htellez; 15.07.2014
comment
Любые значения, которые директива хочет предоставить для использования в выражении ng-change, должны быть указаны при вызове scope.ngChange. Например: scope.ngChange({newValue: item, a: 'something', b: 42}). Затем выражение может использовать открытые значения по мере необходимости: ng-change="bar(newValue, a, b)" - person chrisg; 16.07.2014
comment
Это именно то, чего я не хочу. Как видите, updateModel — это функция внутри директивы. Я хочу, чтобы директива не зависела от того, кто ее использует. Так же, как работает обычная директива angular ng-change. Вам не нужно переопределять угловой ng-directive, чтобы использовать его. - person htellez; 16.07.2014
comment
Извините, Крис, но это просто научит дурным привычкам любого, кто это читает, поэтому я должен проголосовать против. Хтеллез прав. Это тесная связь, и ее нельзя использовать повторно. - person Suamere; 31.10.2016