As you are likely painfully aware, dates are one of the most challenging types of user input to work with. Besides their history of being notoriously under-supported by native HTML, writing validation code can be a time consuming affair. While the HTML5 date input type and third-party widgets can help with the former, date validation can be bolstered by a good JavaScript Date library. The one that I have been using these days is Moment.js. It handles the parsing, manipulating, displaying, and validating of dates and times in JavaScript (JS). Since the HTML5 date type is still not widely supported by browsers, many site owners choose to accept dates via text inputs – a decision that places extra burden on the date validation, while strengthening the case for employing a good JS Date library. In today’s article, we’ll learn how to use Moment.js’s constructor and date validation methods to ensure that user-input dates are legal (i.e. correctly formatted) and valid.
It All Starts with the Parsing
How strictly you treat date strings goes hand-in-hand with your validation approach. For instance, native JS allows for rollovers so that a date of “Feb 31, 2000” is perfectly acceptable; the extra days will be added to the following month. Since Moment.js’s constructor acts as a wrapper for the plain ole’ JS Date object, it is naturally quite forgiving. In short, it checks if the string matches known ISO 8601 formats or RFC 2822 Date time format. Should those fail, it passes the input to the Date(string) constructor.
Due to inconsistent browser implementation of dates, your best bet is to supply your preferred format(s) to the Moment.js () constructor:
//Christmas day - one format
moment("1995-12-25", "YYYY-MM-DD");
//multiple formats
moment("12-25-1995", ["MM-DD-YYYY", "YYYY-MM-DD"]);
Following this rule of thumb makes the parser’s job a lot easier, which facilitates validation as well.
Strict Mode
The Moment.js parser’s forgiving nature offers the possibility of working with partial dates. Problem is, that same behavior can get you into trouble when dealing with exact dates. For instance, the following partial match is enough to qualify as a valid date:
moment('2016 is a date', 'YYYY-MM-DD'); //valid because the year matched!
The way to avoid issues of that sort is to include the strict mode Boolean argument in the constructor invocation. A value of true specifies that the format and input must match exactly, including delimiters:
moment('2016 is a date', 'YYYY-MM-DD', true); //no longer valid!
moment('2016/10/24', 'YYYY-MM-DD', true); //neither is this!
Testing for Date Legality
If you only want to know whether a date string was successfully converted into a proper date object, you can invoke the isValid() method on the moment instance variable:
var aDate = moment(dateElt.value, 'YYYY-MM-DD', true);
var isValid = aDate.isValid();
It returns a boolean: true for valid, false for invalid.
Determining which Date Part Caused the Parser to Abort
Once isValid() comes back false, there is little point in continuing. The best you can do at that point is provide some details to your user. Enter the invalidAt() method. It returns a numeric index, which corresponds one of the following date parts:
- no overflow
- year
- month
- day
- hour
- minute
- seconds
- milliseconds
Here’s some code that would result in a value of “2” for an invalid day:
dateElt.value = '2000-02-30';
var aDate = moment(dateElt.value, 'YYYY-MM-DD', true);
var invalidAt = aDate.invalidAt(); //returns 2 for invalid day
Accessing All of the Parsing Details
To get even more fine-grained details about parsing, you can invoke parsingFlags() on your invalid moment. It will produce an object much like this one:
//moment#parsingFlags for "34/1999extra..."
[object Object]
{
[functions]: ,
__proto__: { },
charsLeftOver: 11,
empty: false,
invalidFormat: false,
invalidMonth: null,
iso: false,
nullInput: false,
overflow: -1,
unusedInput: [
0: "34/",
1: "extra...",
length: 2
],
unusedTokens: [
0: "-",
1: "MM",
2: "-",
3: "DD",
length: 4
],
userInvalidated: false
}
That’s a lot of information, so let’s review each possible attribute and what it pertains to:
- overflow: An overflow of a date field, such as a 13th month, a 32nd day of the month (or a 29th of February on non-leap years), a 367th day of the year, etc.
overflow
contains the index of the invalid unit to match#invalidAt
(see below);-1
means no overflow. - invalidMonth: An invalid month name, such as
moment('Marbruary', 'MMMM');
. Contains the invalid month string itself, or else null. - empty: An input string that contains nothing parsable, such as
moment('this is nonsense');
. Boolean. - nullInput: A
null
input, likemoment(null);
. Boolean. - invalidFormat: An empty list of formats, such as
moment('2013-05-25', [])
. Boolean. - userInvalidated: A date created explicitly as invalid, such as
moment.invalid()
. Boolean. - meridiem: Indicates what meridiem (AM/PM) was parsed, if any. String.
- parsedDateParts: Returns an array of date parts parsed in descending order – i.e. parsedDateParts[0] === year. If no parts are present, but meridiem has value, date is invalid. Array.
Note that not all of the above attributes relate to every Date format and usage. For instance, invalidMonth only comes into play for written months. Moreover, nullInput is very unlikely to be triggered because in JavaScript nulls must be set explicitly.
Conclusion
Now that we know how to validate dates using Moment.js, the next step will be to bridge the UI to Moment.js’s date validation methods to display relevant contextual information to the user.