如何为自定义指令实现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();
        }
    }
}

我想在点击一个项目并且foo的值已经改变时调用ng-change

也就是说,如果我的指令被实现为:

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

我希望在foo的值更新时调用bar

使用上面给出的代码,成功调用了ngChange,但是调用时使用的是foo的旧值,而不是更新后的新值。

解决这个问题的一种方法是在超时时间内调用ngChange,以便在将来foo的值已经改变的时候执行它。但是这个解决方案让我失去了对事情执行顺序的控制,我认为应该有一个更优雅的解决方案。

我也可以在父作用域的foo上使用一个监视器,但是这个解决方案并没有真正给出ngChange方法来实现,而且我被告知监视器是很大的内存消耗者。

是否有一种方法可以使ngChange同步执行没有超时或监视器?

示例:http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview

如果您需要ngModel,您可以在ngModelController上调用$setViewValue,它隐式地计算ng-change。链接函数的第四个参数应该是ngModelCtrl。下面的代码将使ng-change为你的指令工作。

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

为了使你的解决方案工作,请从myDirective的隔离作用域中删除ngChange和ngModel。

这里有一个plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview

tl;dr

在我的经验中,你只需要从ngModelCtrl继承。当您使用ngModelCtrl.$setViewValue

方法时,将自动求值ng-change表达式。
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-changengModelCtrl.$commitViewValue() 期间求值如果你的ngModel的对象引用发生了变化。如果你没有使用trigger参数或者没有精确设置任何ngModelOptions, $commitViewValue()方法会被$setViewValue(value, trigger)自动调用。

我指定如果更改了$viewValue的引用,将自动触发ng-change 。当您的ngModelstringint时,您不必担心它。如果你的ngModel是一个对象,你只是改变了它的一些属性,那么$setViewValue将不会计算ngChange

如果我们从post

开始的代码示例
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
};

经过一番研究,似乎最好的方法是使用$timeout(callback, 0)

在回调执行后自动启动一个$digest周期。

所以,在我的例子中,解决方案是使用
$timeout(scope.ngChange, 0);

这样,不管你的回调函数的签名是什么,它都会像你在父作用域中定义的那样执行。

这里是有这些变化的plunkr: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview

Samuli Ulmanen和lucienBertin的回答明确了这一点,尽管在AngularJS文档中进一步阅读提供了如何处理这个问题的进一步建议(参见https://docs.angularjs.org/api/ng/type/ngModel.NgModelController)。

特别是在你传递对象到$setViewValue(myObj)的情况下。AngularJS文档说明:

当与标准输入一起使用时,视图值将始终是一个字符串(在某些情况下被解析为另一种类型,例如输入[Date]的Date对象)。但是,自定义控件也可以将对象传递给此方法。在这种情况下,我们应该在将对象传递给$setViewValue之前复制一个对象。这是因为ngModel不会对对象进行深度监视,它只会查找身份的变化。如果你只改变了对象的属性,那么ngModel就不会意识到对象已经改变了,也不会调用$parsers和$validators管道。由于这个原因,你不应该改变副本的属性,一旦它被传递给$setViewValue。否则,可能会导致作用域上的模型值发生不正确的变化。

对于我的具体情况,我的模型是一个时刻日期对象,所以我必须先克隆对象,然后调用setViewValue。我很幸运,moment提供了一个简单的克隆方法: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);
        }
    };
}

这里的基本问题是,直到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

最新更新