Difference between on-change and ng-change - javascript

Below is a pretty straightforward setup for a form drop down list. However, the on-change event refuses to fire... unless it's changed to ng-change.
This had me stuck for about an hour, since we use the same setup elsewhere in our site (i.e. model property/list/on-change directive) without fail.
I'm wondering what the difference is between on-change and ng-change and why one works in this instance and the other doesn't?
View
<select id="grArchive"
name="grArchive"
class="form-control"
options="userList"
ng-model="selectedUser"
ng-options="user as user.name for user in userList"
on-change="showSelected()">
</select>
Controller
scope.selectedUser = {};
scope.userList = [];
scope.showSelected = function(){
scope.columns = addColumns;
};
var loadUserList = function(data){
scope.userList = $.map(data, function(item){
return {id: item.id, name: item.firstName + ' ' + item.lastName};
});
}
Just in case: the user drop down populates as expected (screen shot below)

ng-change
ng-change is a directive provided by Angular core API which internally registers an expression to be called when any change happens in $viewValue of ng-model variable (here is the code); assign it an expression such as myFunction(). That provided expression will evaluate inside the underlying controller scope. After calling an expression it runs a digest cycle to make sure bindings are updated on the view. Besides ng-change there are other directives used for events, like ng-focus,ng-mousedown,ng-submit, ng-blur, etc. Here is a list of such directives
on-change
It is a way of calling a JavaScript function on change of input element value. It will search for the function which is globally available in context (obviously it will not call the function registered in angular controller) and evaluate it.

Related

Mousetrap key bindings updating $scope but not detected by $watch.

Basically I have created a function to update a scope variable
// controller.js
$scope.variable = 0;
$scope.$watch('variable', function(){
console.log("change from watch");
});
$scope.increment = function(){
$scope.variable++;
console.log($scope.variable)
}
When I bind a key to this function,
Mousetrap.bind('j', $scope.increment)
the console.log in the browser shows that the variable is incremented when the key, in this case "j", is being pressed, but the $scope.$watch function above is never called and the console.log message "change from watch" is not fired.
When I attach a click handler, however, on the html file,
// index.html
<a ng-click="increment()">{{ variable }}</a>
The variable increases, the console.log shows that the $scope.variable is incremented and the $watch function is being fired as well.
In addition to this problem, {{ variable }} does not change when I use the key binding, yet it changes when I click on it.
My guess is that there is something lacking in Mousetrap that causes the function to fire but not in sync with AngularJS $scope?
Make sure you read about Databinding in AngularJS. Basically, in order for AngularJS to react to changes, something needs to trigger a $digest. The built-in ngClick directive does that under the hood.
When using non-AngularJS APIs (such as Mousetrap), you need to manually wrap the callback in $apply, e.g.:
Mousetrap.bind('j', () => $scope.$apply($scope.increment));

AngularJS doesn't update two-way bound model used in custom directive when model is changed from code called by that directive

You'll have to bear with me on this one, because it's a little involved and isn't answered by the following questions:
Two way binding Angularjs directives isn't working
AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding
Both of those answers are more than 3 years old and no longer apply. Please do not mark this as a duplicate of those questions.
I wish that I could make a simple, reproducible version of this problem, but my Angular is honestly not good enough, and it is a fairly complex setup with lots of meddling from the outside. But the title is correct.
TL;DR version:
User presses escape from input box made by custom directive
Code in parent scope is called through ngKeyup in order to change the value, by updating the bound model
This update is not reflected in the data displayed by the directive
Putting code to do that update inside the directive itself, which directly updates its OWN bound model, to escape the problem of parent-scope bound-model changes not working, does not work.
In fact, even though it is changed inside its own directive controller, it sets the parent-scope model and its own model to the input value instead of the one I'm trying to overwrite with.
And it still doesn't work.
???
Long version with code:
I had a bunch of code I was repeating over and over again in my HTML, and decided to make a custom directive. The code uses many functions from the parent scope. You don't need to know what all these functions do, just note there's a ton of them that happen on click, on blur, etc. So, it looks like this:
template html file
<div ng-if="should_display" class="myclass" ng-class="cssSizeClass">
<div ng-if="allow_edits">
<span
class="text myapp-edit-hover"
ng-click="showEditField = true; captureValue(propertyString,binValue, $event)"
ng-hide="showEditField"
{{ binValue | customNumber: numberPlaceholder }}{{ numberType }}
</span>
<input
ng-show="showEditField"
class="myapp-editable-input"
type="text"
focus-on-show
ng-focus="selectText($event)"
ng-model="binValue"
ui-number-mask="numberPlaceholder"
ui-hide-group-sep
ng-blur="showEditField = false; inputUpdate(propertyString, binValue, 27, false )"
ng-keyup="inputUpdate(propertyString, binValue, $event, false)">
<span class="numberSuffix">{{ numberSuffix }}</span>
</div>
In my directive controller, in order to use those pesky parent-scope functions...
.directive('appDetailCardInput', function() {
return {
restrict: 'E',
templateUrl: '/static/js/directives/mytemplate.html',
scope: { // # = string, = = object, & = function
propertyName: '#',
cssSizeClass: '#',
numberPlaceholder: '#',
numberType: '#',
binValue: '=',
editable: '=',
numberSuffix: '#'
},
controller: function($scope) {
// define local properties
$scope.should_display = $scope.$parent.thing;
$scope.propertyString = 'thing.' + $scope.propertyName;
// HERE, outside functions are "captured"
$scope.captureValue = $scope.$parent.captureValue;
$scope.inputUpdate = $scope.$parent.inputUpdate;
$scope.selectText = $scope.$parent.selectText;
// check if field is editable
if (typeof $scope.editable === 'undefined') {
$scope.allow_edits = true;
}
else {
$scope.allow_edits = $scope.editable;
}
}}
};
});
This actually works 90%! When the bound model in the parent scope is updated outside the Angular digest cycle (the usual case is during a callback from an async messaging service), it updates the displayed value properly. Other things that occur "outside" the directive update it properly.
However. When escape is pressed or a blur happens, or any code is called that updates the value from "inside" the custom directive, no such luck.
I'd like to take a moment here to note that the $event target will blur properly when programmatically told to.
In the case of the escape key, the inputUpdate() function is supposed to set the value back as if someone hadn't typed something in the input. The problem comes here:
// (in inputUpdate function)
$event.target.blur(); // This works
$event.target.value = $scope.previouslyCapturedValue; // This works
setstr = "$scope.thing." + ser + " = " + $scope.previouslyCapturedValue + ";"; // This does not work
eval(setstr);
The eval() changes the parent scope model back, but that isn't reflected in the child scope.
So I tried both methods from the answers above. Adding a $watch in a link function only fires the SECOND time escape is pressed, not the first, so the first time someone hits escape on a field, it doesn't help! Fail.
The Angular docs suggest wrapping the changes to the value in a $timeout, so that the changes are applied outside of the digest cycle. Doing so doesn't help...
$timeout(function() {
console.log('evaluating outside digest supposedly');
$scope.nexttarget = $scope.nextval;
eval($scope.nextstr);
}, 0, false);
Nothing changes, the same behavior happens. The displayed value does not change (although the value that you change when clicking on it does).
So with some more digging, I find out that $event.target.value = $scope.previouslyCapturedValue; is just setting the value of the input, and not updating the actual bound model. That's fine. The bound model just needs to be updated too.
If I could somehow directly overwrite the text displayed on the , that would almost be a worthwhile solution compared to going back to how things were before.
But like I said before, when it is updated exactly the same as eval($scope.nextstr); from an external messaging service, the update happens perfectly correctly.
So at this point in the story I get it into my head: why not 'intercept' the ngKeyup directive and set the model right there inside the directive? I put this in my controller:
$scope.inputUpdate = function(ser, value, $event, dropdown) {
var keyCode = $event.keyCode;
console.log('got inputupdate!');
if ((parseInt(keyCode) == 27)
|| (typeof keyCode === 'undefined')) {
console.log('escape or deblur!');
if (ser !== $scope.dontBlur) {
console.log('going to set it!!!')
$scope.binValue = $scope.$parent.previouslyCapturedValue; // This is the correct value confirmed by console logs
}
}
$scope.$parent.inputUpdate(ser, value, $event, dropdown);
}
Nope! It gets there in code, but it still doesn't display the correct value! In fact, the bound model is updated to what had been typed in, even though it had been programmatically changed back right there!
Why does it update fine from outside its own code from another controller, but NOT from its own code, even if it's being done directly in the directive controller itself in the "intercepted" function?
Can't wrap my head around this. It's like the bound model is "locked" during this time period or something.
Thanks for any guidance.

Call method on Angular scope from Underscore Template

I have an underscore template that is called from an Angular controller. I have a dropdown on the template and a call to onchange on the dropdown. The onchange attempts to calls a method on the scope on the controller. I have tried everything to get the method called in the onchange but doing this
<select onchange="foo(this.value)">
gives me
foo is not defined
and
<select onchange="scope.foo(this.value)">
gives me
scope is not defined
and
<select onchange="$scope.foo(this.value)">
gives me
$scope is not defined
Is it even possible to call a method on the controller in this way?
In the code that is calling the _.template() function, assuming its putting the templated value into a variable and has access to scope:
Find the element in the templated markup: var select = $(templatedVar).find('#selectId');
Add a change event: select.change(function() { //call scope function });
use ngChange instead attribute of onchange
check this link for more info "https://docs.angularjs.org/api/ng/directive/ngChange"

How do I correctly check form validity within a watcher when model data is changed outside of the form in angularjs?

I'm still quite new to AngularJS and I think I'm having trouble understanding the timing with $scope. In a controller, I've setup a watcher for some model data that is bound to various form elements. The watcher fires off an ajax request when the data changes, except if the form is not valid. I'm checking the form validity with myForm.$valid. This is all pretty straight forward, however, except when the model data is updated in the controller, and not the form. The validations runs as expected, but form.$valid still has the previous value, not what it should be with the updated data. For example, if the form was previously valid, then I delete the model value bound to a required input, the watcher will fire because the model data has changed, but when I log the value of myForm.$valid it's value is still true, even though it should be false.
So my question is A. why is this happening?, but more importantly B. what is the correct way to handle what I am trying to accomplish? Would a custom directive make sense?
Here is a simplified example.
HTML:
<div ng-controller="MyCtrl">
<form name="myForm">
<input type="text" name="myField" ng-model="myData" required>
<button type="button" ng-click="myData=''">Delete</button>
</form>
<div>
The watcher says the form is: <strong>{{ formStatus }}</strong>
</div>
</div>
Controller:
myApp.controller('MyCtrl', ['$scope', function($scope) {
$scope.myData = 'abc';
$scope.formStatus = '';
$scope.$watch('myData', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log("my data changed");
console.log("my form valid = ", $scope.myForm.$valid);
$scope.formStatus = $scope.myForm.$valid ? 'Valid' : 'Invalid';
}
});
}]);
Fiddle: http://jsfiddle.net/anpsince83/cK6cc/1/
A custom directive is the right way to go. It's generally recommended for a handful of reasons that watches be put in directives vs controllers. At a high level, one reason is that Angular recommends controllers be thin and only operate as the glue between the view and services. But you're hitting a specific, interesting reason.
Controllers run before Angular directives are linked in. So your controller watch gets added to the watch list before Angular's ngModelWatch() does for myData. ngModelWatch() is where Angular runs $formatters (recalling that $formatters, amongst other things, are where validation is triggered when a change happens to the model).
Since watches are executed in the order in which they were added to the watch list, your controller $watch executes prior to validation being done by $formatters.
If you instead put the same $watch inside a directive it'll be added during link after ngModelWatch() has been added to the watch list. Thus the directive's $watch will execute after validation is done.
Here's an updated fiddle where you can watch the order of execution of $formatters and each $watch. And you'll see the directive version works as expected- running after $formatters.

Angularjs: how to make input[text] ngModel delay valued while typing [duplicate]

This question already has answers here:
Angularjs: input[text] ngChange fires while the value is changing
(8 answers)
Closed 9 years ago.
I have a textbox with ngModel binding, like this:
<input type="text" ng-model="typing" />
and value of this texbox
value: {{ typing }}
I want the model delay to update value while i'm typing. Maybe if I stop type in 500ms, the model will update all value (all things I typed in textbox).
I make some google but no luck. Anyong has any idea? please help.
EDIT
This Angularjs: input[text] ngChange fires while the value is changing doesn't give solution for my case. It bring solution update value after blur, but I want the value update after stop typing, not blur textbox.
EDIT 2 (Answers)
With angular version 1.4, directive ngModelOptions is useful in my case. I can write like this <input ng-model="typing" ng-model-options="{ updateOn: 'default', debounce: {'default': 500, 'blur': 0} }" /> to delay update value to model 500ms in default and update immediately if lost focus.
The tidiest way to handle this is probably to write a directive which wraps up the <input> element and adds the delaying behaviour. Here is a directive I wrote for the same purpose:
angular.module('MyModule')
.directive('easedInput', function($timeout) {
return {
restrict: 'E',
template: '<div><input class="{{externalClass}} my-eased-input" type="text" ng-model="currentInputValue" ng-change="update()" placeholder="{{placeholder}}"/></div>',
scope: {
value: '=',
timeout: '#',
placeholder: '#',
externalClass: '#class'
},
transclude: true,
link: function ($scope) {
$scope.timeout = parseInt($scope.timeout);
$scope.update = function () {
if ($scope.pendingPromise) { $timeout.cancel($scope.pendingPromise); }
$scope.pendingPromise = $timeout(function () {
$scope.value = $scope.currentInputValue;
}, $scope.timeout);
};
}
}
});
This directive would be called in your HTML like so:
<eased-input value="myValue" timeout="500" placeholder="Please enter text..." />
Dissecting the directive:
Timeout Service
This directive uses angular's $timeout service to handle timing: it is an injectable, mockable, idiomatic alternative to calling setTimeout. This service is injected in the directive constructor.
Attributes
The directive accepts three attributes: value, timeout and placeholder.
The value attribute here binds to a variable on the scope of the controller which owns the enclosing 'context'. In this case it binds to myValue, i.e. to $scope.myValue on whichever controller is in charge of this code. It has a two-way binding, denoted by the '=' entry in the scope property of the directive. This means that when this directive updates value, the change is propagated up to the controller which owns the directive; hence, $scope.myValue will change when value is changed inside the directive.
The timeout and placeholder attributes have one-way bindings: the directive reads their values from the attributes but does not alter them. They are effectively configuration values.
HTML Template
The template property on the directive shows the HTML which will be generated in its place once Angular compiles and links it. It's basically just an input element with some special and not-so-special attributes. The value in the input box is bound to the currentInputValue variable on the directive's $scope via ng-model. The change event on the input box is bound to the update function on the directive's $scope via the ng-change directive.
Link function
The guts of the process lie in the link function on the directive: we define an update method. As stated above, this method is bound to the change event of the input box within the directive's HTML template. Thus, every time the user changes the input in the box, update is called.
This method uses the $timeout service. It tells the $timeout service to wait for timeout milliseconds, then to apply a callback which sets $scope.value = $scope.currentInputValue. This is similar to calling setTimeout(function () {$scope.value = $scope.currentInputValue}, timeout).
The $timeout call returns a promise. We are able to cancel a promise p produced by $timeout which is waiting to execute by calling $timeout.cancel(p). This is what update does in its first line: if we have a promise from a previous change event, we cancel it before creating a new one. This means that if we have e.g. a 500ms timeout, and update gets called twice, with the calls 400ms apart, we will only have one promise waiting to fire.
Overall result
The promise, when resolved, sets $scope.value = currentInputValue; i.e. it sets the 'outwardly visible' value property to have the value of the contents of the input box. value will only change -- and external controllers will only see value change -- after a quiescent period of timeout milliseconds, which I believe is the behaviour you were after.
If you're okay with having a second property in your model, you can use $scope.$watch together with a debounce function:
HTML
<input type="text" ng-model="typing" />
<input type="text" value="{{ typed }}" />
Javascript
$scope.$watch('typing', debounce(function() {
$scope.typed = $scope.typing;
$scope.$apply();
}, 500));
You can write your own debounce function, or use an existing one. There's a good implementation here, or, if you happen to be using undescore.js, you're already set.
Here's a jsFiddle example.
UPDATE: Angular 1.3 has now a built-in way to debounce user input: ngModelOptions.

Categories

Resources