ng-click is faster than Angular data binding - angularjs

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.

Related

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

ngChange in custom directive fire twice

Attached custom directive that let using ng-change as attribute.
The problem is, when I using it twice in the same view, I notice that bar method fire twice, I found the problem that the $viewChangeListeners store the same method of the controller twice. Note: notice that maybe I've the same directive twice with two different ng-change methods
Have any one any idea to get over this problem?
HTML:
<my-directive ng-model="foo" items="items" ng-change="bar1(foo)"></my-directive>
<my-directive ng-model="foo2" items="items" ng-change="bar2(foo2)"></my-directive>
Directive:
template: '<div ng-repeat="item in items" ng-click="updateModel(item)">{{item}}</div>',
require: "ngModel",
scope : {
items : "="
},
link: function(scope, element, attrs, ctrl){
scope.updateModel = function(item)
{
ctrl.$setViewValue(item);
}
ctrl.$viewChangeListeners.push(function() {
scope.$eval(attrs.ngChange);
});
}
plunkr sample
Try this one, click on the up (1), then click on the down (1), then click on the up (1). The events fires as following: bar1, bar2, bar1, bar2
When your custom input directive "supports" ngModel - that is to say that it uses the ngModel pipeline to set the view value - it automatically gets the benefit of supporting other directives that use ngModel, like ng-required, ng-maxlength, and ng-change.
And so, the following is not needed:
ctrl.$viewChangeListeners.push(function() {
scope.$eval(attrs.ngChange);
});
In your particular example it actually does not fire bar1()/bar2() handlers twice, because scope.$eval(attrs.ngChange) is evaluated inside the isolate scope that doesn't have bar1() and bar2(). So, perhaps that happened in your testing before you used the isolate scope.
I ran into the same problem but couldn't find a solution on this site or elsewhere. In my case, null was being sent as the value in the additional function call. So my workaround is to handle null in my function:
$scope.bar1 = function(foo){
if(foo != null){
//perform the task
}
};

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

Watch controller model value from inside directive

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

Angularjs + kendoui dropdownlist

I have this directive
angular.module('xxx', [
])
.directive('qnDropdown', [
'$parse',
function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
scope.$watch(attr.qnDropdown, function(source) {
var model = $parse(attr.ngModel);
elem.kendoDropDownList({
dataTextField: "Name",
dataValueField: "ID",
value: attr.value,
select: function(e) {
var item = this.dataItem(e.item.index());
scope.$apply(function() {
model.assign(scope, item.value);
});
},
//template: '<strong>${ data.Name }</strong><p>${ data.ID }</p>',
dataSource: source
});
});
}
};
}]);
Input field is
<input qn:dropdown="locations" ng:model="installation.LocationID" value="{{installation.LocationID}}" />
EVerything works fine but initial value for kendoDropDownList is not filled (value: attr.value).
I suppose I am doing something at wrong place or time but not sure what?
You probably need to use $observe:
Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined. -- docs, see section Attributes.
Here's an example where I used $observe recently. See also #asgoth's answer there, where he uses $watch, but he also created an isolate scope.
I'm still not clear on when we need to use $observe vs when we can use $watch.
Are you sure {{installation.LocationID}} has a value you expect? I was able to copy-paste your code with some tweaks for my situation and the dropdownlist is working wonderfully (thank you for doing the hard work for me!). I'm populating value on the input field and when the directive executes, attr.value has it and Kendo shows it as expected. Perhaps this was an Angular issue a couple versions ago?
I had the same problem, the attr.value was empty. The problem was related to an $http async call being made to get the data. The scope data was not yet populated when the dropdownlist was being defined in the directive.
I fixed this by watching attr.ngModel instead of attr.qnDropdown in the link function of the directive. This way the dropdownlist gets defined when the scope data is populated.

Resources