AngularJS custom validation not firing when changing the model programmatically - angularjs

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/

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;
});
}
}
})

AngularJS: Format DateTimePicker on load

I'm really struggling to get a datetimepicker working in a custom application within Umbraco 7. I've eventually got as far as creating a directive and saving the value back to the model but after hours of trying various examples I still have not been able to find a way to format the date on the initial load. The date format is '1901-02-07T01:05:00Z' until you interact with the datepicker. Here's what I have so far in my directive:
angular.module("umbraco.directives")
.directive('datetimepicker', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var ngModel = $parse(attrs.ngModel);
element.datetimepicker({
format: 'yyyy-mm-dd hh:ii:ss',
autoclose: true
})
.on('changeDate', function () {
scope.$apply(function (scope) {
ngModel.assign(scope, element.val());
});
})
}
}
});
The HTML used is:
<input type="text" ng-model="ShowFrom" datetimepicker />
Does anybody have any ideas on how I would achieve this? I've also tried using parsers and formatters with no luck.
Thanks
As I suspected the answer was to use parsers and formatters. I was originally getting an "unknown format provider" error when trying to use them, it turns out that adding require: 'ngModel' to the directive solved the problem. The $parser function converts the string back to a date for updating the model while the $formatter function takes the model value and formats it appropriately as a string.
Below is the final version of the directive. If anyone else would like to use the code then here is the datepicker this was written for.
angular.module("umbraco.directives")
.directive('datetimepicker', function ($parse, $filter) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
var ngModel = $parse(attrs.ngModel);
element.datetimepicker({
format: 'yyyy-mm-dd hh:ii:00',
autoclose: true
})
.on('changeDate', function () {
scope.$apply(function (scope) {
ngModel.assign(scope, element.val());
});
});
ctrl.$parsers.push(function (value) {
return new Date(Date.parse(value));
});
ctrl.$formatters.push(function (value) {
if (value.indexOf('0001-01-01') < 0)
return value.replace('T', ' ').replace('Z', '');
else
return "";
});
}
}
});

How do I properly set the value of my timepicker directive in AngularJS?

I'm trying to create a timepicker directive in AngularJS that uses the jquery timepicker plugin. (I am unable to get any of the existing angular TimePickers to work in IE8).
So far, I was able to get the directive to work as far as updating the scope when a time is selected. However, what I need to accomplish now is getting the input to display the time, rather than the text of the model's value when the page first loads. See below:
this is what shows:
this is what I want:
Here is my directive:
'use strict';
playgroundApp.directive('timePicker', function () {
return {
restrict: 'A',
require: "?ngModel",
link: function(scope, element, attrs, controller) {
element.timepicker();
//controller.$setViewValue(element.timepicker('setTime', ngModel.$modelValue));
//ngModel.$render = function() {
// var date = ngModel.$modelValue ? new Date(ngModel.$modelValue) : null;
//};
//if (date) {
// controller.$setViewValue(element.timepicker('setTime', date));
//}
element.on('change', function() {
scope.$apply(function() {
controller.$setViewValue(element.timepicker('getTime', new Date()));
});
});
},
};
})
The commented code is what I've attempted, but it doesn't work. I get an error that reads, ngModel is undefined. So, to clarify, when the page first loads, if there is a model for that input field, I want the input to show only the time, as it does after a value is selected.
Thanks.
EDIT:
Ok, after making some trial and error changes, my link function looks like this:
link: function (scope, element, attrs, controller) {
if (!controller) {
return;
}
element.timepicker();
var val = controller.$modelValue;
var date = controller.$modelValue ? new Date(controller.$modelValue) : null;
controller.$setViewValue(element.timepicker('setTime', controller.$modelValue));
//ngModel.$render = function () {
// var date = ngModel.$modelValue ? new Date(ngModel.$modelValue) : null;
//};
if (date) {
controller.$setViewValue(element.timepicker('setTime', date));
}
element.on('change', function() {
scope.$apply(function() {
controller.$setViewValue(element.timepicker('getTime', new Date()));
});
});
},
This doesn't give me any errors, but the $modelValue is always NaN. Here is my controller code:
$scope.startTime = new Date();
$scope.endTime = new Date();
and the relevant html:
<input id="startTime" ng-model="startTime" time-picker/>
<input id="endTime" ng-model="endTime" time-picker />
Is there something else I need to do?
I spent several days trying with the same plugin without getting results and eventually I found another:
http://trentrichardson.com/examples/timepicker/
It works perfectly using the following directive:
app.directive('timepicker', function() {
return {
restrict: 'A',
require : 'ngModel',
link : function (scope, element, attrs, ngModelCtrl) {
$(function(){
element.timepicker({
onSelect:function (time) {
ngModelCtrl.$setViewValue(time);
scope.$apply();
}
});
});
}
}
});
I hope you find useful.

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();
}
});
}
};
}]);

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

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);
});
}
});
}
};
});

Resources