Posted by & filed under User Interface.

I have come to love Twitter Bootstrap. As a developer who is a bit design challenged I really love the structure that it provides. I find I don’t have to make as many decisions which is a real time saver. I also like the light weight jQuery plugins that come with it. One that is missing however is a date picker.

Addy Osmani looks to have done a nice job in implementing a Twitter Bootstrap theme for jQuery UI. However even jQuery UI does not include a date and time picker. Also I really wanted to avoid too many jQuery plugins. So I decided to write my own. Hopefully what it lacks in wow features it makes up for in usability.

I’m also a big fan of Knockout so I used it to implement this solution.

I implemented this in a partial view and coded the javescript so it could be called from the view that included the partial.

In the main view you include this partial just call setupDateTimePicker and pass it the viewModel from the main view.

To get a javascript date object of the date chosen call viewModel.date().

As you can see in this code this control is limited in the dates you can pick with it, but should be easy enough to modify for a wider range of dates. I suppose that is one limitation in the select list method of picking dates.

Click here to download the source and see a working demo.

HTML with Razor markup

<div class="btn-toolbar">
    <div>
        <span>Date &amp; Time Selected:&nbsp;&nbsp;</span>
        <span data-bind="text: month()+' '+day()+', '+year()+' '+hour()+':'+minute()+' '+ampm()"></span>
    </div>
    <hr/>
    <table>
        <thead>
            <tr>
                <th>Month</th>
                <th>Day</th>
                <th></th>
                <th>Year</th>
                <th></th>
                <th>Hour</th>
                <th></th>
                <th>Minutes</th>
                <th></th>
                <th>am / pm</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <div class="btn-group">
                        <button class="btn" data-bind="text: month"><span class="caret"></span></button>
                        <button class="btn dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                        </button>
                        <ul class="dropdown-menu" data-bind="foreach: months" style="min-width: 50px; height: 200px; overflow-y: scroll;">
                            <li><a href="#" data-bind="text: label, click: monthSelect" ></a></li>
                        </ul>
                    </div>
                </td>
                <td>
                    <div class="btn-group">
                        <button class="btn" data-bind="text: day"></button>
                        <button class="btn dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                        </button>
                        <ul class="dropdown-menu" data-bind="foreach: days" style="min-width: 25px; height: 200px; overflow-y: scroll;">
                            <li><a href="#" data-bind="text: $data, click: daySelect" ></a></li>
                        </ul>
                    </div>
                </td>
                <td><span style="font-size: 2em">,</span></td>
                <td>
                    <div class="btn-group">
                        <button class="btn" data-bind="text: year"></button>
                        <button class="btn dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                        </button>
                        <ul class="dropdown-menu" data-bind="foreach: years" style="min-width: 25px; height: 200px; overflow-y: scroll;">
                            <li><a href="#" data-bind="text: $data, click: yearSelect" ></a></li>
                        </ul>
                    </div>
                </td>
                <td>&nbsp;&nbsp;&nbsp;</td>
                <td>
                    <div class="btn-group">
                        <button class="btn" data-bind="text: hour"></button>
                        <button class="btn dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                        </button>
                        <ul class="dropdown-menu" data-bind="foreach: hours" style="min-width: 25px; height: 200px; overflow-y: scroll;">
                            <li><a href="#" data-bind="text: $data, click: hourSelect" ></a></li>
                        </ul>
                    </div>
                </td>
                <td>
                    <div style="display: inline-block; font-size: 2em; margin-top: -5px;">:</div>
                </td>
                <td>
                    <div class="btn-group">
                        <button class="btn" data-bind="text: minute"></button>
                        <button class="btn dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                        </button>
                        <ul class="dropdown-menu" data-bind="foreach: minutes" style="min-width: 25px; height: 200px; overflow-y: scroll;">
                            <li><a href="#" data-bind="text: $data, click: minuteSelect" ></a></li>
                        </ul>
                    </div>
                </td>
                <td>&nbsp;&nbsp;&nbsp;</td>
                <td>
                    <div class="btn-group">
                        <button class="btn" data-bind="text: ampm"></button>
                        <button class="btn dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                        </button>
                        <ul class="dropdown-menu">
                            <li><a href="#" onclick="ampmSelect('am'); return false;" >am</a></li>
                            <li><a href="#" onclick="ampmSelect('pm'); return false;" >pm</a></li>
                        </ul>
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
</div>

Javascript

   var dateTimePickerModel = {
      month: ko.observable('@DateTime.Today.ToString("MMMM")'),
      day: ko.observable(@DateTime.Today.ToString("dd")),
      year: ko.observable(@DateTime.Today.ToString("yyyy")),
      hour: ko.observable(@(DateTime.Now.Hour < 13 ? DateTime.Now.Hour+1 : DateTime.Now.Hour-11)),
      minute: ko.observable('00'),
      ampm: ko.observable('@DateTime.Now.AddHours(1).ToString("tt").ToLower()'),
      days: ko.observableArray([]),
      months: [
         { label: 'January', days: 31 },
         { label: 'February', days: 28, leapdays: 29 },
         { label: 'March', days: 31 },
         { label: 'April', days: 30 },
         { label: 'May', days: 31 },
         { label: 'June', days: 30 },
         { label: 'July', days: 31 },
         { label: 'August', days: 31 },
         { label: 'September', days: 30 },
         { label: 'October', days: 31 },
         { label: 'November', days: 30 },
         { label: 'December', days: 31 }],
      years: [2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032],
      leapyears: [2012, 2016, 2020, 2024, 2028, 2032],
      hours: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
      minutes: ['00','05','10','15','20','25','30','35','40','45','50','55']
   };

   function setupDateTimePicker(viewModel) {
      $.extend(viewModel, dateTimePickerModel);
      setupDays();
      viewModel.date = ko.computed(function() {
         return new Date(viewModel.month() + ' ' +
            viewModel.day() + ', ' +
            viewModel.year() + ' ' +
            viewModel.hour() + ':' +
            viewModel.minute() + ' ' +
            viewModel.ampm());
      });
   }

   function setupDays() {
      var days = @DateTime.DaysInMonth(DateTime.Today.Year, DateTime.Today.Month);
      for(var i=1;i -1) {
               days = month.leapdays;
            } else {
               days = month.days;
            }
            var daysDif = days - viewModel.days().length;
            if (daysDif != 0) {
               var lastDay = viewModel.days()[viewModel.days().length - 1];
               if (daysDif > 0) {
                  for (var j=1; jdaysDif; j--) {
                     viewModel.days.pop();
                  }
               }
               lastDay = viewModel.days()[viewModel.days().length - 1];
               if (viewModel.day() > lastDay)
                  viewModel.day(lastDay);
            }
            break;
         }
      }
   }

   function monthSelect(data) {
      viewModel.month(data.label);
      validateDays();
   }
   function daySelect(data) {
      viewModel.day(data);
   }
   function yearSelect(data) {
      viewModel.year(data);
   }
   function hourSelect(data) {
      viewModel.hour(data);
   }
   function minuteSelect(data) {
      viewModel.minute(data);
   }
   function ampmSelect(data) {
      viewModel.ampm(data);
   }