Monday, March 19, 2007

How to build a simple calendar with JavaScript

While there are lots of JavaScript-based calendar widgets out there, there's not much in the way of explaining how they work for the JS acolyte. I recently had the opportunity of building one from memory (and best of all, for no particular reason), using none of the popular JS libraries. This is the tutorial I wish I had found five years ago.

This series of posts will cover:

  1. how to create a simple calendar view with JavaScript
  2. how to tie the calendar to an HTML element for rendering
  3. how to add next/previous month controls
  4. how to add date picker functionality

Part One: a basic calendar display

We're ready to start laying the groundwork for our calendar widget. Here are the steps we'll be taking:

  1. define some global variables to hold common values
  2. define the calendar object and its arguments
  3. write a method to generate the HTML needed to render the calendar
  4. write a method to return the HTML

The Date object

I used to fear the JavaScript Date object, but it's actually fairly simple. In short, here's what it does:

  1. parses a given date, and provides useful information about that date
  2. if no date is specified, the current date is used as the default

A full discussion of the Date object is beyond the scope of this tutorial. You can find documentation here. However, for the purposes of this project, it's important to understand what the Date object doesn't do:

  • it doesn't know the names of months or days of the week
  • it doesn't know how many days are in a given month
  • it doesn't compensate for leap year
  • it doesn't know the day of the week on which a given month begins

Surprisingly, the Date object isn't used in this widget as much as you'd expect. It's primarily used to determine the current date (if needed) and the starting day of the week for the specified month.

Global variables

As stated above, the Date object doesn't provide us with everything we need, so we have to compensate with a few predefined arrays of values.


// these are labels for the days of the week
cal_days_labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

// these are human-readable month name labels, in order
cal_months_labels = ['January', 'February', 'March', 'April',
                     'May', 'June', 'July', 'August', 'September',
                     'October', 'November', 'December'];

// these are the days of the week for each month, in order
cal_days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
These are intentionally defined in the global scope, as they may be shared with multiple calendar widgets. The naming convention is arbitrary; you may prefer to append them to the calendar constructor to prevent collisions with other variables. Since the Date object returns integers for month (0-11) and day (1-31), we'll be able to use those as indexes for looking up the human-readable labels, as well as the number of days in the specified month.

But wait, how do we compensate for leap year when February is hard-coded at 28 days? Don't worry, it's coming later in this tutorial.

In addition, we'll need a Date object representing the current date, as a fallback:
// this is the current date
cal_current_date = new Date(); 

The Calendar constructor

Our constructor is pretty basic at the moment. I've designed it to take two arguments, a month and year (both integers) which will represent the intial calendar state. If these arguments are missing or null, the global default date object will be used instead.
function Calendar(month, year) {
  this.month = (isNaN(month) || month == null) ? cal_current_date.getMonth() : month;
  this.year  = (isNaN(year) || year == null) ? cal_current_date.getFullYear() : year;
  this.html = '';
}

Right now you might be saying "hey, why the long syntax? Can't we just do a shortcut like this?"

this.month = month || cal_current_date.getMonth();

One of the pitfalls of this method is that zero can be interpreted as false, which means if we specify zero (January) for our month, this expression would use the default month instead.

We also want the option to pass null for one or both of the values. The isNaN() function returns true when passed a null value, which will produce an incorrect result. So we have to test for both conditions: the argument must either be not a number or null for the default to be used.

HTML generation

Here's where we take the date info and stitch together an HTML calendar grid view. Let's start with an empty method definition:

Calendar.prototype.generateHTML = function(){

}

Not familiar with object prototyping in JavaScript? Here's some good reading.

Now let's step through everything this method has to handle.

First day of the week

March 2007 begins on a Thursday, but the Date object doesn't know that. However, if we specifically give it the date of "March 1 2007" to parse, it can tell us that day of the week as an integer (from 0-6). So, let's feed it such a date:

var firstDay = new Date(this.year, this.month, 1);

We can now query the new Date object for the day of the week:

var startingDay = firstDay.getDay();
// returns 4 (Thursday)

So now the widget knows that March 2007 starts on the 5th day of the first week (remember, we're counting from zero like computers do).

Number of days in the month

This one's easy. Just use the numeric month value to look up the value in our days-in-month array:

var monthLength = cal_days_in_month[this.month];

Compensate for leap year

Right, how does the widget know if it's leap year or not? This stumped me until I did a Google code search and found a widely-implemented approach that uses the modulus operator:

if (this.month == 1) { // February only!
  if ((this.year % 4 == 0 && this.year % 100 != 0) || this.year % 400 == 0){
    monthLength = 29;
  }
}

Damn, I'm glad I didn't have to figure that out. Thanks, Google Code!

Constructing the HTML

We're ready to start building the HTML string for our calendar view. First, the header stuff:

var monthName = cal_months_labels[this.month];
var html = '<table class="calendar-table">';
html += '<tr><th colspan="7">';
html +=  monthName + "&nbsp;" + this.year;
html += '</th></tr>';
html += '<tr class="calendar-header">';
for (var i = 0; i <= 6; i++ ){
  html += '<td class="calendar-header-day">';
  html += cal_days_labels[i];
  html += '</td>';
}
html += '</tr><tr>';

It's pretty straightforward: we begin with a table and header containing the month and year. Then we use a for loop to iterate over our human-readable days-of-the-week array and create the first row of column headers.

There are more efficient ways of concatenating HTML, but I thought this was the most clear. And don't gimme no lip about using a TABLE vs. a bunch of floated DIVs — I may be a standardista but I'm not a masochist.

Now for the tricky part, the remaining boxes. We need to make sure that we don't start filling in boxes until we've reached the first weekday of the month, and then stop filling them in when we've reached the maximum number of days for that month.

Since we don't know how many rows we'll need, we'll just generate a safe number of rows — like ten or so — and break out of the loop once we've run out of days. Here we go:

var day = 1;
// this loop is for is weeks (rows)
for (var i = 0; i < 9; j++) {
  // this loop is for weekdays (cells)
  for (var j = 0; j <= 6; j++) { 
    html += '<td class="calendar-day">';
    if (day <= monthLength && (i > 0 || j >= startingDay)) {
      html += day;
      day++;
    }
    html += '</td>';
  }
  // stop making rows if we've run out of days
  if (day > monthLength) {
    break;
  } else {
    html += '</tr><tr>';
  }
}

html += '</tr></table>';

this.html = html;

The real brain-twister is here:

if (day <= monthLength && (i > 0 || j >= startingDay)) {

...which roughly translates to "fill the cell only if we haven't run out of days, and we're sure we're not in the first row, or this day is after the starting day for this month." Whew!

Here's the complete code for our HTML-generating method:

Calendar.prototype.generateHTML = function(){

  // get first day of month
  var firstDay = new Date(this.year, this.month, 1);
  var startingDay = firstDay.getDay();
  
  // find number of days in month
  var monthLength = cal_days_in_month[this.month];
  
  // compensate for leap year
  if (this.month == 1) { // February only!
    if((this.year % 4 == 0 && this.year % 100 != 0) || this.year % 400 == 0){
      monthLength = 29;
    }
  }
  
  // do the header
  var monthName = cal_months_labels[this.month]
  var html = '<table class="calendar-table">';
  html += '<tr><th colspan="7">';
  html +=  monthName + "&nbsp;" + this.year;
  html += '</th></tr>';
  html += '<tr class="calendar-header">';
  for(var i = 0; i <= 6; i++ ){
    html += '<td class="calendar-header-day">';
    html += cal_days_labels[i];
    html += '</td>';
  }
  html += '</tr><tr>';

  // fill in the days
  var day = 1;
  // this loop is for is weeks (rows)
  for (var i = 0; i < 9; i++) {
    // this loop is for weekdays (cells)
    for (var j = 0; j <= 6; j++) { 
      html += '<td class="calendar-day">';
      if (day <= monthLength && (i > 0 || j >= startingDay)) {
        html += day;
        day++;
      }
      html += '</td>';
    }
    // stop making rows if we've run out of days
    if (day > monthLength) {
      break;
    } else {
      html += '</tr><tr>';
    }
  }
  html += '</tr></table>';

  this.html = html;
}

Returning the HTML

By design, the generateHTML method doesn't return the finished HTML string. Instead, it stores it in a property of the calendar object. Let's write a getter method to access that string.

Calendar.prototype.getHTML = function() {
  return this.html;
}

Okay, let's see what we've got!

Using the calendar

The widget may be complex on the inside, but it's ridiculously easy to implement. Here's how to embed a calendar displaying the current month into a web page:

<script type="text/javascript">
  var cal = new Calendar();
  cal.generateHTML();
  document.write(cal.getHTML());
</script>

Now let's set it to September 2009:

<script type="text/javascript">
  var cal = new Calendar(8,2009);
  cal.generateHTML();
  document.write(cal.getHTML());
</script>

The Demo!

Here's our calendar widget in action.

That's nice, but...

We're stuck displaying only one month?

Shouldn't generateHTML and getHTML be called automatically?

And WTF is up with that lame document.write?

Yep, this simple calendar is "simple" alright. Later this week we'll look at a much slicker way to integrate the calendar into webpages and add controls to transform our humble static calendar view into a full-blown datepicker.

9 Comments:

At 2:15 PM, Blogger Portland Hobnob said...

Nice calendar! And thanks so much for the step by step instructions so I actually learn what all the code means.

Question: Is there a way to make this an events calendar? Being new to code, I don't know how to do it myself. I would think that I could hide some code in each post (e.g. 20070704, for July 4th, 2007) and then build a search function into the calendar to find these future events.

Any help you can give would be great! I have looked all over the web for something that would work on the new blogger, and haven't found anything yet.

 
At 5:52 PM, Blogger itsoknow said...

Do you think you can make a calendar like this for my entertainment portal ??
And how much you ask for that? I want to have it.

Thanks!!!

 
At 12:17 AM, Blogger aleh49 said...

it's interested..

http://insomnia-causes-reason-insomnia.blogspot.com/

 
At 8:47 PM, Blogger Jean-Sebastien said...

There's a more efficient way to check the number of days in a month without requiring the verification for leap years :

function daysInMonth(year, month)
{
var dd = new Date(year, month, 0);
return dd.getDate();
}

 
At 10:49 PM, Blogger kanhaiya said...

Thanks, I can easily learn how i create a calendar ,In this i can understand code step by stepin this program.
Kanhaiya.

 
At 4:20 AM, Blogger Vince said...

I have been searching for a calendar script for weeks and I finally came across one that explains it step-by-step. I actually understand the code without having to study it, and it works great. I also added another loop to show the 12 months of the year, instead of just one. Thanks for whoever created the code!

 
At 7:08 AM, Blogger Vince said...

I'm trying to modify this code a little so that it changes the color of the current day to red. How would I do that?

 
At 10:09 PM, Blogger Bigridex said...

This is a good straight forward explanation of how to do a calendar.
I have learned basic java but learning how to apply it, I am still learning. I wish other examples that people write take into consideration they are teaching to people not computers. Thanks

 
At 6:29 PM, Blogger Nechtan said...

Another way to know is how many days each month:

function monthLength(y, m) {
return (new Date(y, ++m, 0)).getDate();
}

// test diogo nechtan console feb.2012
console.log(monthLength(2012, 2)); // 29

 

Post a Comment

<< Home