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.
<!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:
<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:
(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:
_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:
<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:
_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:
_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:
_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:
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:
_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.
_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.
_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
:
_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:
.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:
<link rel="stylesheet" type="text/css" href="jquery.monthly.css">
Usage
Let's now update our plugin instantiation with a couple more options:
<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!