Watch controller model value from inside directive - angularjs

I am trying to have angular watch the $viewValue of a controller from inside a directive.
fiddle: http://jsfiddle.net/dkrotts/TfTr5/5/
function foo($scope, $timeout) {
$scope.bar = "Lorem ipsum";
$timeout(function() {
$scope.bar = "Dolor sit amet";
}, 2000);
}
myApp.directive('myDirective', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, controller) {
scope.$watch(controller.$viewValue, function() {
console.log("Changed to " + controller.$viewValue);
});
}
}
});
As is, the $watch function is not catching the model change done after 2 seconds from inside the controller. What am I missing?

$watch accepts the "name" of the property to watch in the scope, you're asking it to watch the value. Change it to watch attrs.ngModel which returns "bar", now you're watching scope.bar. You can get the value the same way you were or use scope[attrs.ngModel] which is like saying scope["bar"] which again, is the same as scope.bar.
scope.$watch(attrs.ngModel, function(newValue) {
console.log("Changed to " + newValue);
});
To clarify user271996's comment: scope.$eval is used because you may pass object notation into the ng-model attribute. i.e. ng-model="someObj.someProperty" which won't work because scope["someObj.someProperty"] is not valid. scope.$eval is used to evaluate that string into an actual object so that scope["someObj.someProperty"] becomes scope.someObj.someProperty.

Wanted to add: in 1.2.x, with isolated scope, the above wont work. http://jsfiddle.net/TfTr5/23/
A workaround I came up with was using the fact that $watch also accepts a function, so you can access your controller using that.
scope.$watch(
function(){return controller.$viewValue},
function(newVal, oldVal){
//code
}
)
Working fiddle: http://jsfiddle.net/TfTr5/24/
If anyone has an alternative, I would gladly welcome it!

If you want to bind a value inside isolated scope,there are 2 ways to do it.The first way you can use even you don't have isolated scope.Here are the ways:
1) use $attrs.any_attribute and bind it (set in watch)
2) use 2 ways binding ('=') method and set it into listener
if you want more with examples here is a great article
http://www.w3docs.com/snippets/angularjs/bind-variable-inside-angularjs-directive-isolated-scope.html

If you want debounce on a model value, it is worth mentioning the debounce setting in ng-model-option:
<input type="text" ng-model-options="{ debounce: 1000 }" ng-model="search"/>
For example: this watch is trigger 1000 ms after change and reset at new changes.
scope.$watch(attrs.ngModel, function(newValue) { });
https://docs.angularjs.org/api/ng/directive/ngModelOptions

Related

ngModel and How it is Used

I am just getting started with angular and ran into the directive below. I read a few tutorials already and am reading some now, but I really don't understand what "require: ngModel" does, mainly because I have no idea what ngModel does overall. Now, if I am not insane, it's the same directive that provides two way binding (the whole $scope.blah = "blah blah" inside ctrl, and then {{blah}} to show 'blah blah' inside an html element controlled by directive.
That doesn't help me here. Furthermore, I don't understand what "model: '#ngModel' does. #ngModel implies a variable on the parents scope, but ngModel isn't a variable there.
tl;dr:
What does "require: ngModel" do?
What does "model : '#ngModel'" do?
*auth is a service that passes profile's dateFormat property (irrelevant to q)
Thanks in advance for any help.
angular.module('app').directive('directiveDate', function($filter, auth) {
return {
require: 'ngModel',
scope: {
model : '#ngModel',
search: '=?search'
},
restrict: 'E',
replace: true,
template: '<span>{{ search }}</span>',
link: function($scope) {
$scope.set = function () {
$scope.text = $filter('date')($scope.model, auth.profile.dateFormat );
$scope.search = $scope.text;
};
$scope.$watch( function(){ return $scope.model; }, function () {
$scope.set();
}, true );
//update if locale changes
$scope.$on('$localeChangeSuccess', function () {
$scope.set();
});
}
};
});
ngModel is an Angular directive responsible for data-binding. Through its controller, ngModelController, it's possible to create directives that render and/or update the model.
Take a look at the following code. It's a very simple numeric up and down control. Its job is to render the model and update it when the user clicks on the + and - buttons.
app.directive('numberInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<span></span><button>+</button><button>-</button>',
link: function(scope, element, attrs, ngModelCtrl) {
var span = element.find('span'),
plusButton = element.find('button').eq(0),
minusButton = element.find('button').eq(1);
ngModelCtrl.$render = function(value) {
updateValue();
};
plusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue + 1);
updateValue();
});
minusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue - 1);
updateValue();
});
function updateValue(value) {
span.html(ngModelCtrl.$modelValue);
}
}
};
});
Working Plunker
Since it interacts with the model, we can use ngModelController. To do that, we use the require option to tell Angular we want it to inject that controller into the link function as its fourth argument. Now, ngModelController has a vast API and I won't get into much detail here. All we need for this example are two methods, $render and $setViewValue, and one property, $modelValue.
$render and $setViewValue are two ways of the same road. $render is called by Angular every time the model changes elsewhere so the directive can (re)render it, and $setViewValue should be called by the directive every time the user does something that should change the model's value. And $modelValue is the current value of the model. The rest of the code is pretty much self-explanatory.
Finally, ngModelController has an arguably shortcoming: it doesn't work well with "reference" types (arrays, objects, etc). So if you have a directive that binds to, say, an array, and that array later changes (for instance, an item is added), Angular won't call $render and the directive won't know it should update the model representation. The same is true if your directive adds/removes an item to/from the array and call $setViewValue: Angular won't update the model because it'll think nothing has changed (although the array's content has changed, its reference remains the same).
This should get you started. I suggest that you read the ngModelController documentation and the official guide on directives so you can understand better how this all works.
P.S: The directive you have posted above isn't using ngModelController at all, so the require: 'ngModel' line is useless. It's simply accessing the ng-model attribute to get its value.

In AngularJS, How do I create a custom validation directive that takes a $scope variable and compares it for equality to the ng-model?

I'm looking to make a new angular directive which serves the purpose of comparing the input's ngmodel value to some other value, and checking them for equality.
If they are equal, I want this input to be valid. Otherwise, invalid.
<div ng-repeat="one in many">
<ng-form name="somethingToValidate">
<input type="text" ng-model="one.userTypedText"
required mustbeequalto="one.someOtherValue" />
</ng-form>
</div>
That's an example of the "mustbeequalto" directive in use when it's complete.
1) Please be aware that {{one.someOtherValue}} can change at ANY time, so this directive must be aware of the 2-way binding nature of this value.
2) I will be using this inside an ng-repeat, so it should be smart enough to only work within the scope of the particular ng-form containing it.
I think I need a validation directive, but if you think there is a better/moreelegant way, please advise. I've tried creating this directive and have failed miserably.
Write a function that returns the value of ng-model:
require: 'ngModel',
link: function(scope, element, attributes, ngModelController) {
var getModelValue = function() {
return ngModelController.$viewValue;
};
As shown above you can retrieve this via ngModelController.$viewValue. The ngModelController is available as the fourth argument in the link function by requiring it.
Write a function that returns the value of what is passed to the must-be-equal-to attribute:
var getMustBeEqualToValue = function() {
return scope.$eval(attributes.mustBeEqualTo);
};
You can use the $eval method to execute the expression on the current scope to get the correct value.
Write a function that sets the validity:
var setValidity = function (isValid) {
ngModelController.$setValidity('mustBeEqualTo', isValid);
};
Use $watch to execute setValidity everytime the value of ng-model changes:
scope.$watch(getModelValue, function(newValue, oldValue) {
if (newValue === oldValue) return;
setValidity(newValue === getMustBeEqualToValue());
});
Use $watch to execute setValidity everytime the value of what is passed to the must-be-equal-to attribute changes:
scope.$watch(getMustBeEqualToValue, function(newValue, oldValue) {
if (newValue === oldValue) return;
setValidity(getModelValue() === newValue);
});
Demo: http://plnkr.co/edit/w9t0uk6l0HL0QYi40Cth?p=preview
There is room for optimizations, but the example should hopefully be a good start.

How to implement an ng-change for a custom directive

I have a directive with a template like
<div>
<div ng-repeat="item in items" ng-click="updateModel(item)">
<div>
My directive is declared as:
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();
}
}
}
I would like to have ng-change called when an item is clicked and the value of foo has been changed already.
That is, if my directive is implemented as:
<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>
I would expect to call bar when the value of foo has been updated.
With code given above, ngChange is successfully called, but it is called with the old value of foo instead of the new updated value.
One way to solve the problem is to call ngChange inside a timeout to execute it at some point in the future, when the value of foo has been already changed. But this solution make me loose control over the order in which things are supposed to be executed and I assume that there should be a more elegant solution.
I could also use a watcher over foo in the parent scope, but this solution doesn't really give an ngChange method to be implmented and I have been told that watchers are great memory consumers.
Is there a way to make ngChange be executed synchronously without a timeout or a watcher?
Example: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview
If you require ngModel you can just call $setViewValue on the ngModelController, which implicitly evaluates ng-change. The fourth parameter to the linking function should be the ngModelCtrl. The following code will make ng-change work for your directive.
link : function(scope, element, attrs, ngModelCtrl){
scope.updateModel = function(item) {
ngModelCtrl.$setViewValue(item);
}
}
In order for your solution to work, please remove ngChange and ngModel from isolate scope of myDirective.
Here's a plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview
tl;dr
In my experience you just need to inherit from the ngModelCtrl. the ng-change expression will be automatically evaluated when you use the method 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
};
}
};
});
More precisely
ng-change is evaluated during the ngModelCtrl.$commitViewValue() IF the object reference of your ngModel has changed. the method $commitViewValue() is called automatically by $setViewValue(value, trigger) if you do not use the trigger argument or have not precised any ngModelOptions.
I specified that the ng-change would be automatically triggered if the reference of the $viewValue changed. When your ngModel is a string or an int, you don't have to worry about it. If your ngModel is an object and your just changing some of its properties, then $setViewValue will not eval ngChange.
If we take the code example from the start of the 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
};
After some research, it seems that the best approach is to use $timeout(callback, 0).
It automatically launches a $digest cycle just after the callback is executed.
So, in my case, the solution was to use
$timeout(scope.ngChange, 0);
This way, it doesn't matter what is the signature of your callback, it will be executed just as you defined it in the parent scope.
Here is the plunkr with such changes: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview
Samuli Ulmanen and lucienBertin's answers nail it, although a bit of further reading in the AngularJS documentation provides further advise on how to handle this (see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).
Specifically in the cases where you are passing objects to $setViewValue(myObj). AngularJS Documentatation states:
When used with standard inputs, the view value will always be a string (which is in some cases parsed into another type, such as a Date object for input[date].) However, custom controls might also pass objects to this method. In this case, we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realize that the object has changed and will not invoke the $parsers and $validators pipelines. For this reason, you should not change properties of the copy once it has been passed to $setViewValue. Otherwise you may cause the model value on the scope to change incorrectly.
For my specific case, my model is a moment date object, so I must clone the object first before then calling setViewValue. I am lucky here as moment provides a simple clone method: 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);
}
};
}
The fundamental issue here is that the underlying model does not get updated until the digest cycle that happens after scope.updateModel has finished executing. If the ngChange function requires details of the update that is being made then those details can be made available explicitly to ngChange, rather than relying on the model updating having been previously applied.
This can be done by providing a map of local variable names to values when calling ngChange. In this scenario, you can mapping the new value of the model to a name which can be referenced in the ng-change expression.
For example:
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange({newValue: item});
}
In the HTML:
<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>
See: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

ng-click is faster than Angular data binding

I wanted to create an own checkbox input control. It should behave just like <input type="checkbox" ng-model="check" ng-change=”handler()”> with a different look. I created a custom directive with an isolated scope and instead of ng-model I used binding ‘=’ between the directive’s and the controllers’s scope variable and instead of ng-change I used binding ‘&’ between the directive’s on-click handler and a controller’s scope method (handler).
Controller
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
$scope.check = false;
$scope.messages = [];
$scope.onCheckChange = function(value) {
$scope.messages.push('onCheckChange: $scope.check == ' + $scope.check + ' value == ' + value);
}
}])
Directive
.directive('myCheckbox', function() {
return {
restrict: 'A',
scope: {
title: '#',
value: '=',
onChangeHandler: '&'
},
template:
'<span class="my-checkbox"> ' +
'<span class="tickbox" ng-click="click()" ng-class="{\'ticked\': value}" title="{{title}}"></span>' +
'</span>',
link: function(scope, element, attrs) {
scope.click = function() {
scope.value = ! scope.value;
scope.onChangeHandler({value: scope.value});
};
}
};
})
A working plunk is here http://plnkr.co/edit/oIQbqSzcWUShfdTyDBTl?p=preview.
Things work as I would expect with one exception:
User click the checkbox, directive’s scope.click() is called which sets scope.value to the new checkbox value and sends this new value as a parameter to the controller’s $scope.onCheckChange()
But in the controller’s method $scope.onCheckChange() the $scope.check variable does not contain the new value, but the previous one, although the parameter ‘value’ is the new value already.
After ‘a while’ is even the $scope.check variable set to the new value. It seems, that the call from the on-click() is somehow ‘faster’ than the Angular data binding. I have thought about scope.$apply but don’t think this is the case - Angular takes care of the binding nicely but it takes time until it bubbles through…
Questions:
Why is this happening? I would like to know it to avoid some nasty surprises, if there are some waiting for me there in the depths of the Angular see. Similar, not answered question:
angular directive data binding happens after ng-change
Is there a way how to make a new checkbox directive ‘listen’ to native ng-model, ng-change or ng-disable? I would like to approach custom directives the same way as the native ones. Or is it just names of the bindings?
Use ngModel http://plnkr.co/edit/IPWhZ8?p=preview
Your problem was because your handler was triggered before digest cycle finished updating $scope.check.
I still don't know why is it happening, but I've found a workaround: to wrap the handler to a $timeout block without specific delay. It does not matter if it is the handler of the directive or the handler in the controller, both do the trick. The $timeout gives angular time to bubble the binding through.
Anyway, I am still curious why it is like this.

Angular Directive attrs.$observe

I found this Angular Directive online to add a twitter share button. It all seems staright forward but I can't work out what the attrs.$observe is actually doing.
I have looked in the docs but can't see $observe referenced anywhere.
The directive just seems to add the href which would come from the controller so can anyone explain what the rest of the code is doing?
module.directive('shareTwitter', ['$window', function($window) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.share = function() {
var href = 'https://twitter.com/share';
$scope.url = attrs.shareUrl || $window.location.href;
$scope.text = attrs.shareText || false;
href += '?url=' + encodeURIComponent($scope.url);
if($scope.text) {
href += '&text=' + encodeURIComponent($scope.text);
}
element.attr('href', href);
}
$scope.share();
attrs.$observe('shareUrl', function() {
$scope.share();
});
attrs.$observe('shareText', function() {
$scope.share();
});
}
}
}]);
Twitter
In short:
Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.
From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)
$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.
From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)
$compile.directive.Attributes#$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.
<input type="text" ng-model="value" >
<p sr = "_{{value}}_">sr </p>
.directive('sr',function(){
return {
link: function(element, $scope, attrs){
attrs.$observe('sr', function() {
console.log('change observe')
});
}
};
})

Resources