Подтвердить что ты не робот

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

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

<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

4b9b3361

Ответ 1

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

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

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

Здесь plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview

Ответ 2

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

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

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

$timeout(scope.ngChange, 0);

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

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

Ответ 3

TL;DR

По моему опыту вам просто нужно наследовать от 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-chage будет автоматически запускаться , если изменена ссылка $viewValue. Когда ваш ngModel является string или int, вам не нужно беспокоиться об этом. Если ваш ngModel является объектом и вы просто меняете некоторые его свойства, то $setViewValue не будет eval 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
};

Ответ 4

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

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

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

В моем конкретном случае моя модель представляет собой объект date date, поэтому я должен сначала клонировать объект, а затем вызывать 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);
        }
    };
}

Ответ 5

Основная проблема здесь заключается в том, что базовая модель не обновляется до тех пор, пока цикл digest, который произойдет после завершения 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