How do I get my directive to only fire on onchange? - angularjs

I've defined a directive like so:
angular.module('MyModule', [])
.directive('datePicker', function($filter) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$formatters.unshift(function(modelValue) {
console.log('formatting',modelValue,scope,elem,attrs,ctrl);
return $filter('date')(modelValue, 'MM/dd/yyyy');
});
ctrl.$parsers.unshift(function(viewValue) {
console.log('parsing',viewValue);
var date = new Date(viewValue);
return isNaN(date) ? '' : date;
});
}
}
});
The parser seems to fire every time I type a key in my textbox though -- what exactly is the default event, is it keyup, or input? And how do I change it to only fire onchange? It really isn't necessary to fire anymore often than that.
Furthermore, I'm actually manipulating the content of this input using jQuery UI's datepicker. When clicking on the calendar it doesn't seem to trigger the appropriate event that causes the model to be updated/parser to be triggered. I think I can force an event to be fired but I need to know which one.
Trying to use scope.$apply() but that doesn't seem to help any:
.directive('datepicker', function($filter) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
$(elem).datepicker({
onSelect: function(dateText, inst) {
console.log(dateText, inst);
scope.$apply();
}
});
ctrl.$formatters.unshift(function(modelValue) {
console.log('formatting',modelValue);
return $filter('date')(modelValue, attrs.datePicker || 'MM/dd/yyyy');
});
ctrl.$parsers.unshift(function(viewValue) {
console.log('parsing',viewValue);
return new Date(viewValue);
});
}
}
})
I don't think the solution given here works for me because (a) I want to use the datepicker attribute value for choosing a date format or other options, but more importantly, (b) it seems to be passing back a string to the model when I want an actual date object... so some form of parsing has to be done and applied to the ng-model.

Here I created a mo-change-proxy directive, It works with ng-model and it updates proxy variable only on change.
In this demo I have even included improved directive for date-input. Have a look.
Demo: http://plnkr.co/edit/DBs4jX9alyCZXt3LaLnF?p=preview
angModule.directive('moChangeProxy', function ($parse) {
return {
require:'^ngModel',
restrict:'A',
link:function (scope, elm, attrs, ctrl) {
var proxyExp = attrs.moChangeProxy;
var modelExp = attrs.ngModel;
scope.$watch(proxyExp, function (nVal) {
if (nVal != ctrl.$modelValue)
$parse(modelExp).assign(scope, nVal);
});
elm.bind('blur', function () {
var proxyVal = scope.$eval(proxyExp);
if(ctrl.$modelValue != proxyVal) {
scope.$apply(function(){
$parse(proxyExp).assign(scope, ctrl.$modelValue);
});
}
});
}
};
});

Related

how to get current and previous date in angular?

I am using date picker in angular ..which is working fine .But I need when user type "t" or "T" it show current or today date ..And if user type "t-1" ..it show yesterday date ...Same when user type "t+1" it show tomorrow date .
here is my code
http://plnkr.co/edit/UnxLAHmKZU15cqukKqp5?p=preview
angular.module('app',['ui.bootstrap']).controller('cntrl',function($scope){
$scope.open2 = function() {
$scope.popup2.opened = true;
};
$scope.popup2 = {
opened: false
};
}).directive('toDateCheck', function() {
return {
require: 'ngModel',
link: function link(scope, element, attr, ngModel) {
scope.$watch(attr.ngModel, function(val) {
console.log(val)
})
}
}
})
What you're going to want to do is use a parser on your directive. The reason your $watch is not firing is because its not passing validation. Try something like this.
.directive('toDateCheck', function($browser) {
return {
require: 'ngModel',
link: function link(scope, element, attr, ngModelCtrl) {
scope.$watch(attr.ngModel, function(val,l) {
console.log(val,l);
});
ngModelCtrl.$parsers.unshift(function(viewValue){
if(viewValue === 't-1'){
var yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
// Update the textbox to show the new value
element.val(yesterday.toLocaleDateString());
return yesterday;
}
return viewValue;
});
}
}
})

Custom ngModel flag doesn't digest immediately

I'm trying to extend ngModel so that I have $empty flag that works even when the validation is failing. We're using it to show a "clear text" button.
The model override works fine, but I'm noticing that the flag changes don't apply immediately. Calling $digest throws an "in progress" error.
myApp.directive('ngModel', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
var checkEmpty = function() {
ngModel.$empty = (ngModel.$$rawModelValue === undefined ||
ngModel.$$rawModelValue.length === 0);
};
checkEmpty();
element.on('keyup', checkEmpty);
}
}
});
http://jsfiddle.net/sxg7nnwm/2/
You could just simply $watch for changes and set the variable:
scope.$watch(function(){
return ngModel.$isEmpty(ngModel.$viewValue);
},
function(v){
ngModel.$empty = v;
})
EDIT:
Of course, this is inefficient as it registers a $watch.
As correctly pointed out in comments, $viewChangeListeners do not fire for invalid entry. Binding to element events is also bad, since ng-model should be DOM-agnostic.
The approach that could work is to register a $parser that runs first. This would work in most cases - but does not guarantee to work in all, since any other directive is free to register their own parsers (at any time) ahead of our parser and invalidate the value before our parser has a chance to run and re-set $empty.
priority: -100,
require: "ngModel",
link: function(scope, element, attrs, ngModel) {
ngModel.$parsers.unshift(function(v) {
ngModel.$empty = isEmpty();
return v;
});
// needed for first time
var unwatch = scope.$watch(function(){
return isEmpty();
}, function(v){
ngModel.$empty = v;
unwatch();
});
function isEmpty(){
return ngModel.$isEmpty(ngModel.$viewValue);
}
}
Are you forced to use the keyup event? To avoid issues with digest timings you should hook into ngModel's pipeline by using $viewChangeListeners instead.
link: function(scope, element, attrs, ngModel) {
var checkEmpty = function() {
ngModel.$empty = (ngModel.$$rawModelValue === undefined || ngModel.$$rawModelValue.length === 0);
console.log(ngModel.$empty);
};
checkEmpty();
ngModel.$viewChangeListeners.push(checkEmpty);
// element.on('keyup', checkEmpty);
}
Fiddle: http://jsfiddle.net/mwup0s9j/1/
Well, I've found that $timeout works, it sets up my code in the next cycle:
element.on('keyup', function() {
$timeout(checkEmpty);
});

How to access directive parameter in Angular JS?

I'm pretty new to Angular and I have run into a slight problem. I have this simple datepicker directive which works
app.directive('datepicker', function() {
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel){
$(elem).datepicker({
onSelect: function(dateText){
scope.$apply(function(){
ngModel.$setViewValue(dateText);
});
}
});
}
}
});
And the html I use to call it is
<input datepicker data-ng-model="day.date" readonly/>
I would like to be able to change the onSelect function called by the datepicker – hopefully with something like this.
<input datepicker="myOnSelectMethod()" data-ng-model="day.date" readonly/>
Then the directive would look something like this
app.directive('datepicker', function() {
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel){
if(myOnSelectMethod is defined){ //how do I access myOnSelectMethod here?
$(elem).datepicker({
onSelect: myOnSelectMethod();
});
}
else{ //proceed as before
$(elem).datepicker({
onSelect: function(dateText){
scope.$apply(function(){
ngModel.$setViewValue(dateText);
});
}
});
}
}
}
});
So my question is: how do I access the new onSelect function I want to execute from my link function?
Looking through the docs and other SO questions this seems like it should be possible but I haven't been able to make it work. I've come up with an ugly workaround by using an ng-click to activate the datepicker on a given element, but I would like to learn how to make this work if possible. Thanks!
You can check this way:
if( attr["datepicker"] == "myOnSelectMethod" &&
typeof myOnSelectMethod === "function" ){
// ...
}
Or even:
if( typeof scope[attr["datepicker"]] === "function" ){ // Instead of `scope`
// ... // may be any other object,
} // `window` for example

AngularJS custom validation not firing when changing the model programmatically

I have created a custom validator for requiring a date to be in the past. The validation seems to work great when entering the date manually into the field. However, if I enter change the date programmatically (change the model directly as opposed to typing in the field), the validation does not fire.
I believe I am doing the custom validation directive as directed in the documentation. Here is a jsFiddle illustrating the problem. In the fiddle, if you click the "Change date programatically" button, you can see the validation error doesn't get displayed (but it does if you change it manually). Here is the directive code (also in the fiddle):
myApp.directive('pastDate', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
var today = new Date();
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (new Date(viewValue) < today) {
ctrl.$setValidity('pastDate', true);
return viewValue;
}
ctrl.$setValidity('pastDate', false);
return undefined;
});
}
};
});
There are two ways of the model binding, $parsers controls the pipeline of view-to-model direction, and $formatters controls the pipeline of the model-to-view direction. When you update the model in the controller, the change goes through the $formatters pipeline.
I have updated your code to: this, so it handles both ways.
myApp.directive('pastDate', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ctrl) {
function validate (value) {
var today = new Date();
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (new Date(value) < today) {
ctrl.$setValidity('pastDate', true);
return value;
}
ctrl.$setValidity('pastDate', false);
return value;
}
ctrl.$parsers.unshift(validate);
ctrl.$formatters.unshift(validate)
}
};
});
New answer since angular 1.3 provides $validators property.
Since 1.3, $parsers and $formatters are not supposed to set validity anymore, even if it still possible.
Then your code becomes simpler :
myApp.directive('pastDate', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ctrl) {
ctrl.$validators.pastDate = function(modelValue) { // 'pastDate' is the name of your custom validator ...
var today = new Date();
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
return (new Date(modelValue) < today);
}
}
};
});
Updated jsFiddle : http://jsfiddle.net/jD929/53/

Prevent input from setting form $dirty angularjs

I have an ng form on a page. Inside the form I have several controls which need to display a save dialog when the form is dirty, ie form.$dirty = true. However there are some navigation controls in the form I don't want to dirty the form. Assume I can't move the control out of the form.
see: http://plnkr.co/edit/bfig4B
How do I make the select box not dirty the form?
Here's a version of #acacia's answer using a directive and not using $timeout. This will keep your controllers cleaner.
.directive('noDirtyCheck', function() {
// Interacting with input elements having this directive won't cause the
// form to be marked dirty.
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$pristine = false;
}
}
});
Then use it in your form like so:
<input type="text" name="foo" ng-model="x.foo" no-dirty-check>
I used #overthink's solution, but ran into the problem mentioned by #dmitankin. However, I didn't want to attach a handler to the focus event. So instead, I endeavored to override the $pristine property itself and force it to return false always. I ended up using Object.defineProperty which is not supported in IE8 and below. There are workarounds to do this in those legacy browsers, but I didn't need them, so they are not part of my solution below:
(function () {
angular
.module("myapp")
.directive("noDirtyCheck", noDirtyCheck);
function noDirtyCheck() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl) {
var alwaysFalse = {
get: function () { return false; },
set: function () { }
};
Object.defineProperty(ctrl, '$pristine', alwaysFalse);
Object.defineProperty(ctrl, '$dirty', alwaysFalse);
}
};
}
})();
I am also overriding $dirty so it can't be set as dirty either.
Setting the $pristine property to false, only when initializing, works until you call $setPristine() on the form. Then your control has its $pristine back to true and changing the input's value would make your form dirty.
To avoid that, set the $pristine on focus:
link: function(scope, elm, attrs, ctrl) {
elm.focus(function () {
ctrl.$pristine = false;
});
}
Angular only sets the form dirty if the control is pristine. So the trick here is to set $pristine on the control to false. You can do it in a timeout in the controller.
see: http://plnkr.co/edit/by3qTM
This is my final answer. Basically angular internally calls the $setDirty() function of the NgModelController when the input is interacted with, so just override that!
app.directive('noDirtyCheck', function() {
return {
restrict: 'A',
require: 'ngModel',
link: postLink
};
function postLink(scope, iElem, iAttrs, ngModelCtrl) {
ngModelCtrl.$setDirty = angular.noop;
}
})
A variation on #overthink's answer with some additional validation, and inline bracket notation to protect against minification.
"use strict";
angular.module("lantern").directive("noDirtyCheck", [function () {
return {
restrict: "A",
require: "ngModel",
link: function (scope, elem, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
var clean = (ngModelCtrl.$pristine && !ngModelCtrl.$dirty);
if (clean) {
ngModelCtrl.$pristine = false;
ngModelCtrl.$dirty = true;
}
}
};
}]);
I ran into some problems with that implementation, so here is mine (more complex):
app.directive('noDirtyCheck', [function () {
// Interacting with input elements having this directive won't cause the
// form to be marked dirty.
// http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs
return {
restrict: 'A',
require: ['^form', '^ngModel'],
link: function (scope, element, attrs, controllers) {
var form = controllers[0];
var currentControl = controllers[1];
var formDirtyState = false;
var manualFocus = false;
element.bind('focus',function () {
manualFocus = true;
if (form) {
window.console && console.log('Saving current form ' + form.$name + ' dirty status: ' + form.$dirty);
formDirtyState = form.$dirty; // save form's dirty state
}
});
element.bind('blur', function () {
if (currentControl) {
window.console && console.log('Resetting current control (' + currentControl.$name + ') dirty status to false (called from blur)');
currentControl.$dirty = false; // Remove dirty state but keep the value
if (!formDirtyState && form && manualFocus) {
window.console && console.log('Resetting ' + form.$name + ' form pristine state...');
form.$setPristine();
}
manualFocus = false;
// scope.$apply();
}
});
}
};
}]);

Resources