Monday, March 31, 2014

Using MomentJS to Extend the jQuery UI TimePicker Spinner Widget

In my last post, I used several jQuery UI Spinner widgets to create a Time Picker widget. At the time, I was more interested in making the widgets work together than providing a robust getter/setter function. So I cobbled together a simple string parser to split up the time value and set each spinner to the desired value. Additionally, the widget assumed the input format was a 12-hour clock which is not always desirable. So, in this post, I'll add some more features to provide more flexibility and options related to the format. While I don't like adding too many external dependencies, the MomentJS library does everything I need plus a pile of other great features. I generally use it in every project that has any kind of date/time display, manipulation, entry, etc. That said, I made an effort to gracefully fallback to the simple string method I originally wrote for anyone who doesn't want the extra 9Kb of gzip'd Javascript loading.

To review, the current TimePicker has this implementation for getting the current value of the widget:


      _value: function() {
         var hour = this.$hour.val(),
             min = this.$minute.val(),
             ampm = this.$ampm.val();

         return hour + ':' + min + ' ' + ampm;
      }



And this chunk of code handles parsing an incoming value and setting each spinner:


      _parse: function( val ) {
         var parts = val.split( /[: ]/ ),
             hour, min, ampm;

         if ( parts.length < 2 ) return;

         hour = parts[0];
         min = parts[1];
         ampm = parts[2];

         this.$hour.paddedspinner( 'value', +hour );
         this.$minute.paddedspinner( 'value', +min );
         this.$ampm.ampmspinner( 'value', ampm == 'AM' ? 0 : 1 );

      }



The goal is to retain those sections of code but 1) handle a 24-hour clock and 2) detect Moment and use it to both parse the incoming value or create a Moment instance to return the current value of the widget. Before doing that, we need to know the format to use for the widget. I decided to use a format string that aligns with Moment to determine whether to use a 12 or 24 hour clock:


   $.widget('osb.timepicker', {

      options: {
         format: 'hh:mm A' // 12-hour format
                           // HH:mm triggers 24-hour
      },

      _init: function () {
         ...

         // Set a flag to check through out code 
         this.hour24 = ( this.options.format.indexOf( 'HH' ) > -1 );

         ...
      }
      
      ...
   }


Near the top of the _init function, that format string is parsed to determine the clock format. With that flag set, we can add blocks around every spot that has to use the AM/PM spinner and decides how to interpret the hours spinner. Getting the current value isn't too bad - just a little more code to check the clock format and switch between Moment and a basic string:


      _value: function() {
         var hour = +this.$hour.val(),
             min = +this.$minute.val(),
             ampm = this.hour24 ? '' : this.$ampm.val(),
             val;

         if ( window.moment ) {
            // MomentJS included

            hour = ( ampm == 'PM' && hour < 12 ? hour + 12 : hour );
            val = moment({ hour: hour, minute: min });

         } else {
            // Fallback to simple string only

            val = hour + ':' + min;
            if ( !this.hour24 )
               val += ' ' + ampm;
         }

         return val;
      }



When creating the Moment instance, you can specify the hours and minutes. Hours need to be in the 24-hour clock format so a little conversion is necessary if the widget is operating in 12-hour clock mode.

Setting the value is a little longer since I wanted to also accept a Javascript Date object. The constructor for Moment only needs the format string if the time is a string, otherwise, the second argument should be dropped. Additionally, the same conversion from 24-hour to 12-hour clock mode is necessary after calling the Moment hour() function:


      _parse: function( val ) {

         var tm, parts,
             hour, min, ampm;

         if ( window.moment ) {
            // MomentJS included

            if ( val instanceof Date )
               tm = moment( val );
            else
               tm = moment( val, this.options.format );

            if ( !tm.isValid() ) return;

            hour = tm.hour();
            min = tm.minute();

            if ( !this.hour24 ) {
               ampm = hour >= 12 ? 'PM' : 'AM';
               hour = hour > 12 ? hour - 12 : hour
            }

         } else {
            // Fallback to simple handling

            parts = val.split( /[: ]/ );
            if ( parts.length < 2 ) return;

            hour = parts[0];
            min = parts[1];

            if ( !this.hour24 ) {
               ampm = parts[2] || 'AM';
            }
         }

         this.$hour.paddedspinner( 'value', +hour );
         this.$minute.paddedspinner( 'value', +min );

         if ( !this.hour24 ) {
            this.$ampm.ampmspinner( 'value', ampm == 'AM' ? 0 : 1 );
         }
      }



Again, the original string parser remains such that, if Moment is not present, it will be used to find the components of the time. There are a few other changes that were required for switching between the two clock formats which I did not show the code for here. These dealt with rendering the spinners (don't need AM/PM in 24-hour mode) and rolling over the hours properly.

The advantage of including Moment is that the library provides utilities to work with the the output from the picker in a variety of ways including reformatting the time and manipulating its value:

  
   
   $( '#picker' ).timepicker();

   // Can pass in a Date, String, or Moment instance
   $( '#picker' ).timepicker( 'value', '8:25 AM' );
   
   // time is a MomentJS instance
   time = $( '#picker' ).timepicker( 'value' );

   time.format( 'HH:mm' ); // 08:25
   time.add( 'm', 5 );
   time.format( 'h:mm A' ); // 8:30 AM



These changes are a nice improvement to the widget. While probably not as robust as it could be, its definitely a step in the right direction. The updated widget and examples are available on my jQuery UI extensions project at GitHub.