Lugo Labs

Create a month picker jQuery plugin

Demo

Our task today is to create a month and year user friendly picker as a jQuery plugin. Let's call our plugin Monthly (check the demo above).

The users should be able to click any HTML element, but usually a link or button, and be presented with a popup where they can select a year and a month. The plugin should update the clicked element content with the selected month and year, and call a callback provided. The popup should at that stage close.

You can find the final result on the Tutorial repository on Github.

The HTML

Whenever I need to generate HTML via JavaScript, I create a HTML file so that I can test as I go along. Let's add the code below to a index.html file.

html
<!DOCTYPE html>
<html>
<head>
  <title>jQuery Monthly</title>
  <style>
    body {
      font-family: sans-serif;
    }

    .content {
      padding: 5em;
      text-align: center;
    }
  </style>
</head>
<body>

  <div class="content">
    <a href="#monthly" id="monthly"></a>
  </div>

</body>
</html>

This creates an empty HTML file with a content div and a link element we'll use as our popup trigger element.

The HTML for the picker popup could look like this (append that to the content element:

html
<div class="monthly-wrp">
  <div class="years">
    <select>
      <option>2015</option>
      <option>2014</option>
      <option>2013</option>
      <option>2012</option>
      <option>2011</option>
    </select>
  </div>

  <table>
    <tr>
      <td><button data-value="0">January</button></td>
      <td><button data-value="1">February</button></td>
      <td><button data-value="2">March</button></td>
      <td><button data-value="3">April</button></td>
    </tr>
    <tr>
      <td><button data-value="4">May</button></td>
      <td><button data-value="5">June</button></td>
      <td><button data-value="6">July</button></td>
      <td><button data-value="7">August</button></td>
    </tr>
    <tr>
      <td><button data-value="8">September</button></td>
      <td><button data-value="9">October</button></td>
      <td><button data-value="10">November</button></td>
      <td><button data-value="11">December</button></td>
    </tr>
  </table>
</div>

First, we wrap all the markup within a monthly-wrp element. Then we show the years list at the top, following by the months buttons inside a table with data-value containing the month index.

We'll try to generate the HTML via JavaScript next.

The Plugin

Let's create a JavaScript file jquery.monthly.js and the skeleton plugin:

js
(function($) {

  $.fn.monthly = function(options) {

    var months = options.months || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],

      Monthly = function(el) {
        this._el = $(el);
      };

    Monthly.prototype = {
    };

    return this.each(function() {
      return new Monthly(this);
    });

  };

}(jQuery));

Most of the code here is recommended by jQuery plugin creator. I like to keep the JavaScript code within class-like functions, which is I created the Monthly function.

This JavaScript class can be instantiated with the HTML element, which is at the heart of any jQuery plugin. We store the element inside the instance variable _el to reference later (the underscore (_) prefix indicates a private variable.

The months variable holds the month names; note how they can also be overriden if we pass months via options.

When the plugin is first instantiated we want to update the trigger element's text with the first month and year in the list:

js
_init: function() {
  this._el.html(months[0] + ' ' + options.years[0]);
}

This function takes the month and year and show them on the element with a space between. It is added to the Monthly.prototype object.

Before we go any further, let's test our code so far. On our index.html file we add references to jQuery and some code to instantiate the plugin:

html
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="jquery.monthly.js"></script>

<script>
  $(function() {
    $('#monthly').monthly({
      years: [2015, 2014, 2013, 2012, 2011]
    });
  });
</script>

If we view our index.html file in a browser now, we should see our monthly element showing January 2015.

The HTML generation

Time now to generate the popup container. Let's add another method to the Monthly.prototype object:

js
_render: function() {
  var linkPosition = this._el.position(),
    cssOptions = {
      display: 'none',
      position: 'absolute',
      top: linkPosition.top + this._el.height() + (options.topOffset || 0),
      left: linkPosition.left
    };
  this._container = $('<div class="monthly-wrp">')
    .css(cssOptions)
    .appendTo($('body'));
}

Our popup should appear underneath the trigger element, so we give it a absolute position based on the element's position. The topOffset option will offset the container vertically by the specified amount. We'll use the _container instance variable to append the fragments of the popup.

Next, we generate the years list, so another method for the Monthly.prototype:

js
_renderYears: function() {
  var markup = $.map(options.years, function(year) {
    return '<option>' + year + '</option>';
  });
  var yearsWrap = $('<div class="years">').appendTo(this._container);
  this._yearsSelect = $('<select>').html(markup.join('')).appendTo(yearsWrap);
}

We also keep a reference to the years list in the _yearSelect variable, as we'll need it later.

Lastly, the months:

js
_renderMonths: function() {
  var markup = ['<table>', '<tr>'];
  $.each(months, function(i, month) {
    if (i > 0 && i % 4 === 0) {
      markup.push('</tr>');
      markup.push('<tr>');
    }
    markup.push('<td><button data-value="' + i + '">' + month +'</button></td>');
  });
  markup.push('</tr>');
  markup.push('</table>');
  this._container.append(markup.join(''));
}

We create the months in three rows of four months inside a table element, great for storing tabular data.

Let's call the three render functions we created from the Monthly function body, which now looks like this:

js
var Monthly = function(el) {
    this._el = $(el);
    this._init();
    this._render();
    this._renderYears();
    this._renderMonths();
};

If we refresh the index.html file now, the popup will be added to the body of the page, but it will not be visible as we specified display: none in its CSS style. We'll show the popup when the user clicks on the element, so let's do that now.

The Events

All the events will be declared within one method:

js
_bind: function() {
  $(document).on('click', $.proxy(this._hide, this));
  this._el.on('click', $.proxy(this._show, this));
  this._yearsSelect.on('click', function(e) { e.stopPropagation(); });
  this._container.on('click', 'button', $.proxy(this._selectMonth, this));
}

We use the $.proxy utility method to attach events to our elements. When the user clicks on the trigger element, we show the popup; when the user clicks anywhere in the document, we hide it.

js
_hide: function() {
  this._container.css('display', 'none');
}

The _hide method is triggered when the users click anywhere in the document. To avoid this when the years list and months buttons are clicked, we need to call stopPropagation on events triggered by the list and buttons.

js
_show: function(e) {
  e.preventDefault();
  e.stopPropagation();
  this._container.css('display', 'inline-block');
}

Now let's handle the month selection by adding another method to Monthly.prototype:

js
_selectMonth: function(e) {
  var monthIndex = $(e.target).data('value'),
    month = months[monthIndex],
    year = this._yearsSelect.val();
  this._el.html(month + ' ' + year);
  if (options.onMonthSelect) {
    options.onMonthSelect(monthIndex, year);
  }
}

We fetch the month index cached in the data store of the buttons to get the selected month name. The year is the current value of the years list. We then change the text on the trigger element to display the current selection, and call the callback onMonthSelect passed with the options.

The CSS

To make the popup look a bit better we add some styles to a jquery.monthly.css file:

css
.monthly-wrp {
  border: 1px solid #eee;
  padding: 1em;
  top: 6px;
  z-index: 1000;
  box-shadow: 0 0 5px rgba(153, 153, 153, 0.2);
  border-radius: .2em;
}

.monthly-wrp:before {
  content: '';
  border-bottom: 6px solid #fff;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  position: absolute;
  top: -6px;
  left: 6px;
  z-index: 1002;
}

.monthly-wrp:after {
  content: '';
  border-bottom: 6px solid #eee;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  position: absolute;
  top: -7px;
  left: 6px;
  z-index: 1001;
}

.monthly-wrp .years {
  margin-bottom: .8em;
  text-align: center;
}

.monthly-wrp .years select {
  font-size: 1em;
  width: 100%;
}

.monthly-wrp .years select:focus {
  outline: none;
}

.monthly-wrp table {
  border-collapse: collapse;
  table-layout: fixed;
}

.monthly-wrp td {
  padding: .1em;
}

.monthly-wrp table button {
  width: 100%;
  border: none;
  background: #F7EEEE;
  font-size: .8em;
  padding: .6em;
  cursor: pointer;
  border-radius: .2em;
}

.monthly-wrp table button:hover {
  background: #EFDDDD;
}

.monthly-wrp table button:focus {
  outline: none;
}

Then we add the reference in the head element of the index.html file:

html
  <link rel="stylesheet" type="text/css" href="jquery.monthly.css">

Usage

Let's now update our plugin instantiation with a couple more options:

html
<script>
  $(function() {
    $('#monthly').monthly({
      years: [2015, 2014, 2013, 2012, 2011],
      topOffset: 6,
      onMonthSelect: function(m, y) { alert('Month: ' + m + ', year: ' + y); }
    });
  });
</script>

If we refresh the browser, we can see how clicking on the trigger link, brings up (or down in this case) the nicely styled popup; changing the year and month updates the text of the element and alerts us of the selected month index and year. Nice!