In most of the UI’s I work on lately, it’s necessary to provide start and end date selection. The typical use case is for setting date ranges for searching and storage of effective dates.
Being that these are typically Angular applications, tying into Angular’s form validation makes a lot of sense.
Validation is Angular is handled like any other directive. The difference is that you tie into a model’s change and then use the built-in validity services to set the validity of the model value based on your validation logic. For dates, you can simply set max/min directives and set the proper input textbox to a type of “date.” The problem with this though, is that then you are constrained to Angular’s expected ISO date format. To avoid this, and to make things place nicer with server-side deserilization of date types, I stick to using a type of “text.” This affords the greatly level of flexibility, but it also means you have to provide your own validation.
To facilitate ranges, I have created two directives. One is simply date-before and the other is date-after. Both directives expect either a model property or primitive. Additionally, you can pass an array of model properties or primitives. Each directive requires ngModel, and performs the requisite validation. Date-before expects the ngModel value to be before the parsed “before” value and date-after expects the ngModel value to be after the parsed “after” value. Additionally, the directives will look for an attribute called “date-or-equals” to also factor in “or equals” into the validation.
Here is the code for one of the directives. They are nearly identical, so I’ll discuss this one a bit.
var dateBefore = function () { var directive = { require: 'ngModel', link: function (scope, el, attrs, ctrl) { var isInclusive = attrs.dateOrEquals ? scope.$eval(attrs.dateOrEquals) : false, validate = function (val1, val2) { if (val1 === undefined || val2 === undefined) return; var isArray = val2 instanceof Array; var isValid = true; var date1 = new Date(val1); if (isArray && val2.length > 0) { for (var i = 0; i < val2.length; i++) { if (val2[i] !== undefined) { var date2 = new Date(val2[i]); isValid = isValid && (isInclusive ? date1 <= date2 : date1 < date2); } if (!isValid) break; } } else { if (val2 !== undefined) { var date2 = new Date(val2); isValid = isInclusive ? date1 <= date2 : date1 < date2; } } ctrl.$setValidity('dateBefore', isValid); }; // Watch the value to compare - trigger validate() scope.$watch(attrs.dateBefore, function () { validate(ctrl.$viewValue, scope.$eval(attrs.dateBefore)); }); ctrl.$parsers.unshift(function (value) { validate(value, scope.$eval(attrs.dateBefore)); return value; }) } } return directive };
As you can see, the directive is pretty simple. It encompasses a watch on the attribute to trigger the validation when the related property changes, and thus triggers the validate() method. It also will trigger validate() when the ngModel parsers are trigger. Parsers are triggered whenever the $viewValue is updated. Think of this process as tying into the defacto watch.
The validate method evaluates the dateBefore expression to get the underlying watched property/value. If the value is an array, then we iterate over the array comparing the $viewValue against each value in the array. If the value is not an array, we make the single comparison. Also, no comparisons are made against undefined values.
From there, we just call the ngModel’s $setValidity method to either set or clear the valid/invalid flags that ngForm will make available in our view.
Check out the included fiddle to see how this works and how we can cascade our validation. Additionally, the filter illustrates using Angular’s $filter service to get dates into the format that our server will expect. Also note what I have described as “cascading” for Date3, it depends on being “after” Date1 and Date2.
The full source can be seen on Github.
There’s a plunk if you prefer Plunker and pen if your prefer Codepen.
This looks great. Do you have a GitHub repo that you’re maintaining where we could reference in the future?
I don’t. You know, I hadn’t even thought about that, but that’s a great idea. I’ll start putting working demos/source over to github and link to them.
See my latest blog post. I’ve added a github repo along w/ Plunker demos of this, with more to come, to make forking/pulling/etc easier. Thanks again for asking!
Awesome, I’ll check it out and be able to give you credit. I’ve been using the directives all day and they have been a huge help. Only change I made was on the field checks to return when no values present, I modified to this:
if (!val1|| !val2) return;
It catches more cases I needed such as null, “”, etc.
That’s a good change. The only reason I had this:
if (val1 === undefined || val2 === undefined)
was because it was similar code I used for a numeric validator I had made previously where a value of ‘0’ would unnecessarily bypass with the truthy-type check. Truthy check makes more sense for a proper existence check.
The ‘undefined’ will no longer be undefined after first entry, so yeah, it would be annoying in that it effectively would force ‘required’ condition from that point forward.
I’ll update that in git too as it makes more sense.
We don’t need a single lines of java script code for it, we can achieve it by add the html attributes like
max-date=’model.enddate’
min-date=’model.startdate’
see for more detail:
http://www.advancesharp.com/blog/1198/angularjs-bootstrap-datepicker-enable-disable-dates-with-configuration-exapmle
Yes, when I wrote this directive, ui-datepicker (now uib-datepicker) did not support this feature.
However, max-date/min-date are more of a UI mechanism. Just using max-date and min-date, you do not actually know what the error is because the only $error attribute that will be set is “dateDisabled.” You have to add additional type=”date” and max/min attributes to the form input in order to know what the actual error is.
Additionally, not everyone uses uib-datepicker. In that case, yes, you would need your own validation just like this directive. My directive also allows one to pass in an array of dates to compare against. This actually was one of my main motivations for writing this directive – it’s more comprehensive than the (currernt) uib-datepicker validation options.