我有一个指令的模板像
<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-change
在ngModelCtrl.$commitViewValue()
期间求值如果你的ngModel的对象引用发生了变化。如果你没有使用trigger参数或者没有精确设置任何ngModelOptions, $commitViewValue()
方法会被$setViewValue(value, trigger)
自动调用。
我指定如果更改了$viewValue
的引用,将自动触发ng-change
。当您的ngModel
是string
或int
时,您不必担心它。如果你的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