5
\$\begingroup\$

I created a jQuery plugin that converts a text field to a 3 drop down, datepicker. It allows the user to select a Day, Month and Year rather than type the date in an input box.

The original input element is hidden and a callback function is provided where the user can format the value for the input box. The id and name attributes are replaced by the original name and id of the element postfixed with Day, Month and Year respectively.

I would like your inputs on the code below. It may be anything from "writing better comments", "architectural improvements" to "performance improvements" or anything that comes to your mind.

JavaScript

/**
jQuery Date picker.
Convert a text box into a 3 drop down date picker with day, month and year as 
drop downs.

Allowed Parameters:
startDate: restrict the user to this Start date.
endDate: restrict the user to this End date.
dateFormat: a function that returns a string that will be used to fill the 
            hidden input box.

Gautham PJ
*/
(function ($) {

    'use strict';

    // The function that handles the Datepicker.
    $.fn.datepicker = function (userOptions) {

        var $this,
            $dayElement,
            $monthElement,
            $yearElement,

            // Keep track of the Months and the number of days in them.
            MONTHS = [
                { month: 0, name: 'January', days: 31 },
                { month: 1, name: 'February', days: 28 },
                { month: 2, name: 'March', days: 31 },
                { month: 3, name: 'April', days: 30 },
                { month: 4, name: 'May', days: 31 },
                { month: 5, name: 'June', days: 30 },
                { month: 6, name: 'July', days: 31 },
                { month: 7, name: 'August', days: 31 },
                { month: 8, name: 'September', days: 30 },
                { month: 9, name: 'October', days: 31 },
                { month: 10, name: 'November', days: 30 },
                { month: 11, name: 'December', days: 31 }
            ],

            // The default options for the plugin.
            defaults = {
                startDate: new Date(),
                endDate: new Date(),
                'class': 'datepicker',
                dateFormat: function (day, month, year) {
                    return day + '/' + month + '/' + year;
                }
            },

            // Merge the default and User options into "options".
            options = $.extend({}, defaults, userOptions),

            // Create the Day, Month and Year drop downs necessary for the 
            // Date picker and add it to the page before the selected element.
            createHTMLElements = function () {

                var name = $this.prop('name');

                $dayElement = $('<select>')
                    .prop({ id: name + 'Day', name: name + 'Day' })
                    .addClass(options.class + ' ' + name + 'Day');

                $monthElement = $('<select>')
                    .prop({ id: name + 'Month', name: name + 'Month' })
                    .addClass(options.class + ' ' + name + 'Month');

                $yearElement = $('<select>')
                    .prop({ id: name + 'Year', name: name + 'Year' })
                    .addClass(options.class + ' ' + name + 'Year');

                $this.before($dayElement);
                $this.before($monthElement);
                $this.before($yearElement);
            },

            // Check if the Starting Year has been selected.
            isStartYearSelected = function () {
                return $yearElement.val() == options.startDate.getFullYear();
            },

            // Check if the Ending Year hsa been selected.
            isEndYearSelected = function () {
                return $yearElement.val() == options.endDate.getFullYear();
            },

            // Check if the Starting Month has been selected.
            isStartMonthSelected = function () {
                return $monthElement.val() == options.startDate.getMonth();
            },

            // Check if the Ending Month has been selected.
            isEndMonthSelected = function () {
                return $monthElement.val() == options.endDate.getMonth();
            },

            // Check if the drop down contains the value that needs to be selected.
            selectValueIfExists = function ($element, value) {

                if (value) {
                    if ($element.find('option[value=' + value + ']').length > 0) {
                        $element.val(value);
                    }
                }
            },

            // Populate the Year drop down with the values or Options.
            populateYear = function () {

                var i,
                    $option,
                    previousSlectedValue = $yearElement.val();

                $yearElement.empty();
                for (i = options.startDate.getFullYear();
                        i <= options.endDate.getFullYear(); i++) {

                    $option = $('<option>').val(i).text(i);
                    $yearElement.append($option);
                }

                selectValueIfExists($yearElement, previousSlectedValue);
            },

            // Populate the Month values based on the selected year.
            populateMonth = function () {

                var i,
                    startMonth = 0,
                    endMonth = 11,
                    $option,
                    previousSlectedValue = $monthElement.val();

                // Check for Leap Year
                if (parseInt($yearElement.val(), 10) % 4 === 0) {
                    MONTHS[1].days = 29;
                } else {
                    MONTHS[1].days = 28;
                }

                // Check for the Start and End Year and limit the Selectable days.
                if (isStartYearSelected()) {
                    startMonth = options.startDate.getMonth();
                }
                if (isEndYearSelected()) {
                    endMonth = options.endDate.getMonth();
                }

                // Empty and Populate the Month field.
                $monthElement.empty();
                for (i = startMonth; i <= endMonth; i++) {

                    $option = $('<option>').val(i).text(MONTHS[i].name);
                    $monthElement.append($option);
                }

                selectValueIfExists($monthElement, previousSlectedValue);
            },

            // Populate the Day drop down based on the month and year selected.
            populateDay = function () {

                var i,
                    startDay = 1,
                    endDay = MONTHS[$monthElement.val()].days,
                    $option,
                    previousSlectedValue = $dayElement.val();

                if (isStartYearSelected() && isStartMonthSelected()) {
                    startDay = options.startDate.getDate();
                }

                if (isEndYearSelected() && isEndMonthSelected()) {
                    endDay = options.endDate.getDate();
                }

                $dayElement.empty();
                for (i = startDay; i <= endDay; i++) {

                    $option = $('<option>').val(i).text(i);
                    $dayElement.append($option);
                }

                selectValueIfExists($dayElement, previousSlectedValue);
            },

            // Update the drop down values.
            update = function () {

                populateYear();
                populateMonth();
                populateDay();

                if (typeof options.dateFormat === 'function') {

                    var value = options.dateFormat.call($this, $dayElement.val(),
                        parseInt($monthElement.val(), 10) + 1, $yearElement.val());
                    $this.val(value);
                }

            },

            // Initialize the components and perform the necessary steps to 
            // get the Date picker ready.
            init = function () {

                options.startDate.setFullYear(
                    parseInt(options.startDate.getFullYear(), 10) - 30
                );

                createHTMLElements();
                $this.hide();

                update();

                // Add Event Handlers for all the elements.
                $yearElement.change(update);
                $monthElement.change(update);
                $dayElement.change(update);

            };

        // Return the selected element for jQuery Chaining.
        return this.each(function () {
            $this = $(this);
            init();
        });

    };

}(jQuery));

HTML

<input type="text" name="dob" id="dob" />

To call the plugin in the HTML page

$(function () {
    'use strict';
    $('#dob').datepicker();
});

JSFiddle Link

\$\endgroup\$
5
\$\begingroup\$

One thing that caught my attention:

// Check for Leap Year
if (parseInt($yearElement.val(), 10) % 4 === 0) {

I reckon you know that % 4 === 0 is not a reliable check for a leap year. Although as far as I am aware all leap years fulfill this rule, not all years fulfilling the rule are leap years.

I suggest that you try to avoid building calendar logic into your code as much as possible - it is always a pitfall, calendar logic is just too complicated. By the way, your code only supports one specific type of calendar, the one that is being used in western countries.

Alternatively you could try something like building a "test" date from the selected $yearElement and verify if JavaScript accepts it as a valid date to determine whether or not the 29. of February should be supplied.

Update Since JavaScript silently accepts an invalid date but represents it incorrectly, you can use that for your check:

if (new Date(selectedYear, 01, 29).getMonth() == 1) {
    // add 29 to the list
}
\$\endgroup\$
  • \$\begingroup\$ Thanks chiccodoro, good find. Based on the accepted answer from link, I have updated the Leap Year checking logic. Is there a library to strictly check of such Date related things?? I don't want my plugin to have dependencies, but I am open to suggestions.. \$\endgroup\$ – LearningDeveloper Apr 7 '15 at 11:28
  • \$\begingroup\$ chiccodoro, I searched around for error detection in creating a new Date() element in JavaScript, but am not able to find a correct answer. It would be helpful if you can give a little push in the right direction. \$\endgroup\$ – LearningDeveloper Apr 9 '15 at 3:43
  • \$\begingroup\$ I understand. The Javascript Date class is a mess. If you create a new Date(2015, 01, 29) it actually creates a valid date but it actually results in the 1st of March. This in turn is the catch you can use as an indicator: If new Date(2015, 01, 29).getMonth() returns 2, it is not a leap year. If new Date(2015, 01, 28).getMonth() returns 1, it is a leap year. See also this fiddle: jsfiddle.net/eajxn3we/1 \$\endgroup\$ – chiccodoro Apr 9 '15 at 12:38
  • \$\begingroup\$ But I have to admit that, given the poor Javascript calendar support, my advice to use Javascript Date to build calendar logic might not be very... \$\endgroup\$ – chiccodoro Apr 9 '15 at 12:39
  • \$\begingroup\$ Ok, understood.. For now i think i am ok with the date. I wanted to know if there are any performance fixes that could be done or additional major functionality that could be provided to the user for easier use of the plugin. \$\endgroup\$ – LearningDeveloper Apr 24 '15 at 4:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.