AngularJs provides a convenient two way bindings to wire up your model and view. But sometimes you would like to intercept that binding.
I’ve come across a case that I need to prompt for user about change for a dropdown field. When user click yes, it will reveal some extra UI. When user click no, it will revert the dropdown back to its previous.
Here I need:
- an value that the extra UI can watch on to reveal the UI for change
- an old value to revert to
- a way to update the model without trigger change
- when the model update from others (not the dropdown), it updates the UI
The solution would be to introduce another model value (“view model”) and try to wire it with the real model.
+--------+ +----------+
|dropdown+------->|View model+-------+
+--------+ +----------+ v
^ +---------+
| |confirm? |
| +----+----+
+--------+ +----+-----+ |
|Others +------->| model |<------+
+--------+ +----------+
^
| watch
+----+-----+
|Extra UI |
+----------+
So here we are the actual dropdown would bind to the view model sView
first
<select ng-model="sView" ng-change="confirm()" ...>...</select>
At the controller, the confirm()
method would play with the real model s
and view model sView
$scope.confirm = (){
$modal.open(...).result.then(updateModel, updateViewModel);
}
function updateModel(){
$scope.s = $scope.sView;
}
function updateViewModel(){
$scope.sView = $scope.s;
}
$scope.$watch('s', function(){
$scope.sView = $scope.s;
});
In Angular POV, you should wrap this up in a directive so that it is re-usable. The desired html code would look like
<select ng-model="sView"
defer-change="confirm()"
defer-change-delegate="s" ...>...</select>
So how can we listen to the ngChange
event? Take a look at the ngChange
directive source code:
var ngChangeDirective = valueFn({
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
ctrl.$viewChangeListeners.push(function() {
scope.$eval(attr.ngChange);
});
}
});
So it just use the $viewChangeListeners
, which would be available using ngModelController
. Even better, all directive components which use ngModel
would have this available. That’s good!
So our little defer-change
directive would finally look like:
app.directive("deferChange", function($modal){
return{
restrict: 'A',
require: 'ngModel',
scope: {
viewModel: '=ngModel',
model: '=deferChangeDelegate',
defer: '&deferChange'
},
link: function(scope, el, attrs, ctrl){
function updateModel(){
scope.model = ctrl.$modelValue;
}
function updateViewModel(){
scope.viewModel = scope.model;
}
ctrl.$viewChangeListeners.push(function(){
scope.deferChange()
.then(updateModel, updateViewModel);
});
scope.$watch('model', updateViewModel);
}
}
});
Note that when updating model, we use ctrl.$modelValue
instead of scope.viewModel
as it is more steadily available when deferring.
Click here for the demo.