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.