Cancelling old requests in ng-change with debounce - angularjs

I have a requirement to fetch data through a web service for the search parameter which I type in a textbox. Currently it is working fine with debounce in ng-model-options and it fires a request after the time I specify.
I couldn't find that does angular cancel the previous incomplete request automatically once a new one is fired.
If not, how should I handle this as each key press will fire a request after the time period I specified.
The HTML is
<input type="text" class="form-control" name="inpTeachers" ng-model="inpTeachers" id="inpTeachers" autocomplete="off" required ng-change="GetTeachers(frmAddSubjectMain)" ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }" placeholder="Ex. Nancy Chang" />
And the method GetTeachers is just a simple method changing some scope variables after a http request.
Should I be cancelling the old http request if another ng-change is fired.

If you might want to close a fired request, you can use the timeout property of the http's config object, giving it a number (milliseconds) or a Promise.
function GetTeachers(frmAddSubjectMain){
var canceler = $q.defer();
yourService.retrieveTeachers(canceler).then(function() {
/* here is your result */
});
}
And in your service, you might need something like:
$http.get('/teachers', {timeout: canceler.promise});
Whenever you would like to cancel the ongoing request (if it isn't finished yet), just call:
canceler.resolve();

Related

Debounce an ng-change event in angular

What's a good way to debounce an ng-change event in angular? I know about the debounce available in ng-model options, but I want my model to update right away as its changed because the user has the option to submit this data, and I don't want any information to be missing from the post because the model was waiting for the debounce. I do want to add a debouncer on my ng-change event or the function it calls. The html for my element
<div ng-change="saveProg()" text-angular ng-model="message" required></div>
I'd like to debounce the saveProg function so it doesn't run once for every character typed, but once every second or so. My function looks like this
$scope.saveProg = function() {
ProgressService.saveProg({
topic: $scope.topic,
message: $scope.message
})
}

Detect if the user fills the form with the autofill option of google chrome

If anyone can guide me on how to detect if the user fills the form with the autofill option of google chrome.
I have a directive where each time the user fills in the field and change the blur sends an event to google analytics.
But also I need to detect whether the user has filled the form with the autofill option of chrome and push the data for each of the fields to google analytics.
Part of my directive:
element.bind('blur', function (e) {
if ((e.target.value !== 0) && typeof value !== 'undefined') {
if (_.has(ga_data, 'sendEvent')) {
analyticsService.sendEvent(ga_data.sendEvent);
}
if (_.has(ga_data, 'action') && ga_data.action === 'blur') {
analyticsService.sendEvent(ga_data);
}
}
});
You can use two way data binding here and watch the ng model on the form field for changes
<form method="post">
First name:<input type="text" name="fname" ng-model="user.fname"/><br />
Last name: <input type="text" name="lname" ng-model="user.lname"/><br />
E-mail: <input type="text" name="email" ng-model="user.email"/><br />
Phone: <input type="text" name="phone" ng-model="user.phone"/><br />
Address: <input type="text" name="address" ng-model="user.address"/><br />
</form>
Then inside your angular controller you can do something of this sort.
angular.module('app', []).controller('AppController', function($scope){
$scope.user = { };
$scope.$watch('user', function(nv, ov){
console.log(nv);
}, true);
});
There might be some cases that you need to handle though, to prevent sending multiple requests because $watch function will be triggered every time the value in the text field changes.
Here is a fiddle which triggers $watch when any value in a form field changes, be it via autofill or manual entry by user.
In this case, the way to detect when Chrome auto-fills a form instead of the user doing it is by detecting the absence of an event having occurred, such as the keyup event. Consider the block of HTML and Javascript below. I have the text input field marked with a data attribute that is initially set to false. If the user fills out anything in the form by typing, then the attribute is set to true. The moment when you record whether or not the user filled out the form is on form submit. Then you can check the fields of the form and see if the user entered the input his or herself.
<form onsubmit="dosomething()">
<label for="somefield"></label>
<input type="text" id="somefield" onkeyup="this.setAttribute('data-entered', (this.value != '').toString());" data-entered="false" />
<input type="submit" value="submit"/>
</form>
The reason why you need to use a keyboard event and send the information when the form is submitted is because you can only tell if auto-fill took place when any of the fields have values even when the user typed nothing in. This part is less about code, and is more about what needs to be done so to take a proper measurement.
Based on input-directive src, angular sets up a listener for cases of change, input, paste etc.
And whenever the browser autofills the input element, this listener is called and viewValue is commited through array of $parsers as of here ngModel directive src.
So eventually you can avoid additional scope.$watch and just rely on $parsers to send ga track event just on linking phase of each input element with directive. And btw don't forget to destroy you parser func right after first usage (i.e. browser autofill), so it will not spam further on viewValue change.
Here's an example:
angular
.module('app', [])
.directive('detectPrefill', function() {
return {
require: 'ngModel',
link: {
pre: function(scope, element, attrs, ctrl) {
function detectPrefill (viewValue) {
//send GA data
//...
// just checking that detectPrefill func is destroyed after first usage
viewValue && console.log(viewValue);
ctrl.$parsers.splice(
ctrl.$parsers.indexOf(detectPrefill),
1
);
return viewValue;
}
ctrl.$parsers.push(detectPrefill);
}
}
};
});
Hope this helps.

How to cancel debounce in ng-model-options in AngularJS

<form name="editForm">
Title<input ng-model="task.title" type="text" name=taskTitle ng-model-options="{ updateOn: 'default' ,debounce{'default':2000000} }">
<a ng-click="UpdateTask(task.title)">SAVE</a>
<a ng-click="editForm.$rollbackViewValue();">DISCARD</a>
</form>
Since the debounce value is very long, when I click "DISCARD", the ng-model will not reflect its changes.
However, what I want is that when I click "SAVE", either I can change the debounce value or make it expired so the changes will be reflected immediately.
I can't find cancelDebounce() in AngularJs document, anyone would like to provide a solution? Thanks
I'd suggest you to have one scope variable which will have value of debounce like $scope.myDebounce = 2000000, then do create one discard function inside controller that will first reset the be-bounce value to 0, then do rollback the form changes. Reset the myDebounce variable to its actual value in next digest cycle.
Markup
<form name="editForm">
Title
<input ng-model="task.title"
type="text"
name=taskTitle
ng-model-options="{ updateOn: 'default', debounce: {'default': myDebounce } }">
<a ng-click="UpdateTask(task.title)">SAVE</a>
<a ng-click="editForm.$rollbackViewValue();">DISCARD</a>
</form>
Code
$scope.myDebounce = 2000000; //somewhere in controller
$scope.discard = function (form) {
$scope.myDebounce = 0; //resetting debounce to get quick `ng-model` update
form.$rollbackViewValue();
$timeout(function(){
//setting the actual debounce value to old one in next digest
$scope.myDebounce = 2000000;
});
}
I think what you really need is NgModelController.$commitViewValue();
From the angular doc
$commitViewValue()
Commit a pending update to the $modelValue.
Updates may be pending by a debounced event or because the input is waiting for a some future event defined in ng-model-options. this method is rarely needed as NgModelController usually handles calling this in response to input events.

Why does $digest() is not being called automatically from Parse callback?

I have a select2 drop down list that is bound to a data in my scope ($scope.makes).
It Is "living" inside an ng-modal.
I update $scope.makes inside a promise, which is a Parse.com call (parse.cloud.run) but after that it is over the DDL is not refershed with the new data.
But if I add the call of $scope.$digest() it is working.
Why is that?
This is the modal controller's relevant part:
Parse.Cloud.run('getCarMakes', {}, {
success: function (results) {
console.log('C');
if (results===undefined){
console.log('The query has failed');
return;
}
for (var i=0;i<results.length;i++){
$scope.makes.push(results[i]);
}
$scope.$digest();
If i remove the last line - the dropdown list is not being refreshed.
Here's the HTML part of the Modal that is relevant
<label class="control-label" for="MakeSelect">Make </label>
<select class="form-control col-md-2" id="MakeSelect" name="make" ui-select2 ng-model="carDetails.make">
<option ng-repeat="make in makes" value="{{ make }}">{{ make}}</option>
</select>
That's because success is out of Angular's scope, you need to explicitly tell Angular that something was updated it must run $digest or $apply(which internally calls $digest) to update it's scope too.
Angular will account for only those model changes which are done inside AngularJS’ context, it has no idea about callback and can't update itself, as it would if change was inside it's scope.
For more information on how you should tell Angular to update it's scope, with $digest() or $apply() read this article or this question $apply vs $digest in directive testing.

AngularJS: revert form on modal cancel?

I'm trying to use AngularJS 1.3.0's new $rollbackViewValue() method in ngModelController to cancel changes to a form in a modal popup or persist them when I close the modal. I'm using BootstrapUI for the $modal service.
I think I'm on the right track, but there is something that isn't quite working properly:
In my controller, I've got:
$scope.updateCharge = function (charge) {
var scope = $scope.$new();
scope.charge = charge;
var modalInstance = $modal.open({
templateUrl: 'client/app/views/providers/charges/updateCharge.html',
scope: scope
});
modalInstance.result.then(function () {
scope.charge.$save({providerId: providerId, chargeId: charge.id});
});
};
In my template, I have the following:
<form name="form" novalidate ng-model-options="{updateOn: 'save', debounce: {'save': 0 }}" class="form-horizontal" role="form">
<div class="modal-body">
<div class="form-group">
<label class="col-sm-3 control-label" for="chargeName">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" required ng-model="charge.code" id="chargeName"/>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-disabled="form.$invalid" ng-click="form.$broadcast('save'); $close()">Update</button>
<button class="btn btn-warning" ng-click="form.$rollbackViewValue(); $dismiss('cancel')">Cancel</button>
</div>
</form>
Generally speaking, this seems to work. When I click on cancel, my changes are reverted. When I click on Update, the modal closes but I do not see my updates in the scope.charge object.
I would have expected that my scope.charge object would be updated prior to the modal closing.
Am I using the ng-model-options incorrectly?
If I add a separate 'Apply' button that only does a form.$broadcast('save'), I see my scope object properly updated. So I am presuming that my $close() is being called prior to the event being processed by the ng-model-options. How can I avoid this race condition?
You can to use the $rollbackViewValue() method to revert changes but I think that is not the intention.
$rollbackViewValue(); Cancel an update and reset the input element's
value to prevent an update to the $modelValue, which may be caused by
a pending debounced event or because the input is waiting for a some
future event.
If you have an input that uses ng-model-options to set up debounced
events or events such as blur you can have a situation where there is
a period when the $viewValue is out of synch with the ngModel's
$modelValue.
In this case, you can run into difficulties if you try to update the
ngModel's $modelValue programmatically before these debounced/future
events have resolved/occurred, because Angular's dirty checking
mechanism is not able to tell whether the model has actually changed
or not.
The $rollbackViewValue() method should be called before
programmatically changing the model of an input which may have such
events pending. This is important in order to make sure that the input
field will be updated with the new model value and any pending
operations are cancelled.
The normal use case is to copy the model, optionally to persist the model and, if all is ok, refresh the model.
_this = this;
this.edit = function() {
this.modelToEdit = angular.copy(this.originalModel);
}
this.save = function () {
service.save(modelToEdit).then(function(savedModel) {
_this.originalModel = savedModel;
});
}
Or you can backup the model and restore when cancel
_this = this;
this.edit = function() {
this.backupModel = angular.copy(originalModel);
}
this.cancel = function() {
this.originalModel = this.backupModel;
}
this.save = function() {
service.save(this.originalModel).then(function(data) {}, function(error) {
_this.originalModel = _this.backupModel;})
}
I can see a few problems with your code:
ngModelOptions.updatedOn is meant to be a DOM event, i.e. click, blur, mouseenter, mouseover, etc,
The form controller does NOT have a $broadcast method, so it's never emitting an event.
I think the fact that it sort of works is because there is not type="button" on the <button> so they are considered as "submit" inside the form. And the model is updated because of that.
I suggest you use a simplified version, and
remove the ng-model-options from the form.
add a type="submit" to the Update button, and remove the form.$broadcast
add a type="button" to the Cancel button.
Am not sure how it would work with the modal, but here's a plunkr with ng-if: http://plnkr.co/edit/m37Fd0NybpnfslNkvJnO
I did not end up using any of the options suggested as answers, because the reality is, angular 1x has no "proper" way of doing what I want. Angular uses 2way binding, and yes I can write fancy directives to make life easier but infact it just makes the html looks even more complicated.
I settled with the suggested way as per many threads on the forum and that is to use angular.copy and then use the cloned model in your html. When you submit changes, set the cloned model to the original model.
There was heaps of examples on here on how to use angular.copy. And it works well.

Resources