Display Relevant MomentJS Date Validation Messages

By Rob Gravelle

In the Date Validation Using Moment.js tutorial, we learned how using a JS Dates library such as Moment.js can make date parsing, validation, and formatting so much simpler. In particular, we reviewed its main date validation methods - isValid(), invalidAt(), and parsingFlags() - ordered by level of detail. In today's follow-up, we'll present contextualized validation information based on the results of these methods.

Separation of Concerns

You've no doubt heard of the "Separation of Concerns" concept in Web development, most likely in regards to keeping your HTML, CSS, and JavaScript separate. However, the Separation of Concerns paradigm goes further than that. JS coders have, as a group, developed numerous anti-patterns, which are common solutions to a recurring problem that are usually ineffective and turn out to be demonstrably highly counterproductive. One of the worst anti-patterns in the realm of modern application development is the coupling of the business and presentation tier code. It's a bad idea because as soon as you have to change something in either the logic or front-end, you've got to sort through piles of code to update references to the new functions and/or elements.

3 tier application structure - licensed under the Creative Commons Attribution 3.0 Unported license

The mixing of validation code and presentation elements is subject to the very same issues. Luckily, as it turns out, the solution to this particular problem is quite simple: don't display messages within your validation function(s). Instead, return the relevant information to the calling function to handle as it sees fit.

The validateDates() Function

Here's a generic validation function that accepts one or more objects containing an element's id and value. We don't need to supply the elements themselves in order to return contextual information about where the error originated as the date string itself is all that's required by Moment.js. Hence, our validateDates() function iterates over each supplied object and passes the date string (stored in the val attribute) to the three parameter moment constructor. It accepts the date, a format string, and a strict mode boolean argument. The function returns a boolean of true when all of the supplied dates are valid; otherwise, it returns an errs object containing the id of the offending element, along with an array of error messages. This allows us to place the focus on the element in question.

Here is the variable initialization code as well as a call to validateDates() with start and end dates:

function validateDates() {
  var dates     = Array.prototype.slice.call(arguments),
      errs      = {
        eltId: '',
        msgs:  []
      },
      dateParts = ['year',
                   'month',
                   'day',
                   'hour',
                   'minute',
                   'seconds',
                   'milliseconds'],
      isValid   = //presented in next code snippet...
}

var start      = document.getElementById('start'),
    end        = document.getElementById('end'),
    datesInfo  = validateDates({
       id:  start.id,
       val: start.value
     }, {
       id:  end.id,
       val: end.value
    });

Mapping the element's value to the val attribute allows us to format the date string from a number of sources, such as a calendar widget or from three separate date part fields (i.e. day, month, year).

Iterating Over Supplied Arguments

Having converted the function arguments into a true Array using slice() in the code above allows us to iterate over it using any of the array iteration functions or looping constructs. I found that the every() function works well because it executes a callback function once for each element of the array until it either

  1. encounters a callback that returns a false value, at which point, the every method immediately returns false, or
  2. returns true upon completion.

Inside our callback function we can invoke the strict moment() constructor and check whether or not the supplied date was valid. If so, the callback returns true, causing every() to immediately move on to the next date. Otherwise, it's time to go into more detail using the parsingFlags() method. The errs object is populated with the source element id and a basic error message identifying the bad value:

isValid = dates.every(function(date) {
   var aDate = moment(date.val, 'YYYY-MM-DD', true);
   
   if ( aDate.isValid() ) { 
     return true; 
   }
   else { //get more info
     errs.eltId       = date.id;
     errs.msgs[0]     = "There were errors in the date '" + date.val + "':";
     var parsingFlags = aDate.parsingFlags();
     
     //assign error messages...
     
     return false;
   }
});

Assigning Detailed Error Messages

In the Date Validation Using Moment.js tutorial, we saw the structure of the object returned by parsingFlags(). We can check each of its attributes to produce a corresponding error message. Since it is quite possible to have more than one, messages are appended to an array:

      //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 
      //-1 means no overflow.
      if (parsingFlags.overflow > -1)
        errs.msgs.push('Invalid ' + dateParts[parsingFlags.overflow] + '.');
      //invalidMonth: An invalid month name, such as moment('Marbruary', 'MMMM');. 
      //Contains the invalid month string itself, or else null.
      if (parsingFlags.invalidMonth)
        errs.msgs.push("'" + parsingFlags.invalidMonth + "' is an invalid month name.");
      //empty: An input string that contains nothing parsable,
      //such as moment('this is nonsense');. Boolean.   
      if (parsingFlags.empty)
        errs.msgs.push("A required date was missing or unparsable.");
      //nullInput: A null input, like moment(null);. Boolean.
      if (parsingFlags.nullInput)
        errs.msgs.push("You supplied a null value.");
      //invalidFormat: An empty list of formats, such as moment('2013-05-25', []). Boolean.
      if (parsingFlags.invalidFormat)
        errs.msgs.push("The following formats were empty: " + parsingFlags.invalidFormat);
      //userInvalidated: A date created explicitly as invalid, 
      //such as moment.invalid(). Boolean.
      if (parsingFlags.userInvalidated)
        errs.msgs.push("A date was created explicitly as invalid.");
      //unusedTokens: array of format substrings not found in the input string
      if (parsingFlags.unusedTokens.length)
        errs.msgs.push("The following date parts were not found in the input string: " + parsingFlags.unusedTokens);
      //unusedInput: array of input substrings not matched to the format string
      if (parsingFlags.unusedInput.length)
        errs.msgs.push("The following input was not utilized: " + parsingFlags.unusedInput); 
      
        return false;
     }
  });
  
  return isValid || errs;
}

As stated earlier, validateDates() returns true or an errs object.

Testing the validateDates() Function

Here is the output produced by various input values. In the first test case, both dates are valid, so the script proceeds to calculate the time elapsed between the start and end dates. Case two and three abort with errors:

Testing "2017-01-01" and "2017-05-03":
Time elapsed between "2017-01-01" and "2017-05-03":
0 years, 4 months, 0 weeks, 2 days

Testing "2017-01-01" and "2017-02-30":
There were errors in the date '2017-02-30':
Invalid day.

Testing "2017-01-01" and "bad date!":
There were errors in the date 'bad date!':
A required date was missing or unparsable.
The following date parts were not found in the input string: YYYY,-,MM,-,DD
The following input was not utilized: bad date!

I've posted the full code in Codepen so that you can examine it more closely.

Conclusion

In this tutorial, we utilized the Moment.js library's isValid() and parsingFlags() date validation methods to present contextualized validation information. Best of all, we did it in a way that decoupled presentation elements from the validation code. By doing so, we could swap out the text inputs for a more complex widget with minimal changes.



Rob Gravelle

Rob Gravelle resides in Ottawa, Canada, and has built web applications for numerous businesses and government agencies. Email him.

Rob's alter-ego, "Blackjacques", is an accomplished guitar player, that has released several CDs and cover songs. His band, Ivory Knight, was rated as one of Canada's top hard rock and metal groups by Brave Words magazine (issue #92) and reached the #1 spot in the National Heavy Metal charts on ReverbNation.com.



Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •  
  •  
Thanks for your registration, follow us on our social networks to keep up-to-date