A few years ago, I wrote a calendar control for a Code Project article. Although I’ve used it myself several times, and despite the fact that people still appear to be using it, I can’t help but cringe every time I look at it again. Every time, the thought that comes to mind is “It could have been so much cleaner in jQuery…”.
Well, the time has come to bite the bullet and see how it can be pulled off again. Rather than going over the whole thing in one go though, we’re going to take it in steps over a number of posts to implement a feature or a group of features, so we can look each in slightly more detail. This is as much for my benefit as everyone else’s. If anyone spots anything weird, inefficient, or badong in these posts, please let me know. I like to think of this as public code review, so, you know. Bring it on.
We can rebuild him – we have the technology
The posts will be divided as follows:
- In part 1 (this post), we will deal with the display of a simple static calendar.
- In part 2, we will add the facility to pick a date, and to switch the month on display.
- In part 3, we will add some sanity checks, and the ability to display events on given days.
And with that, on to the post itself.
Our Target
The image above shows an example of the calendar we’re trying to build. You can find the jQuery plugin, the style sheet, and a working example here. In the source code for the example, I’ve used and commented all the optional fields.
Generating the Mark-Up
The body of the calendar is a table. Generating each element individually isn’t recommended, as it can get slow. Instead, we’re taking advantage of the fact that we know, roughly, how many cells we need (a single month will never span more than 6 rows on a grid 7 cells wide). If you look at line 35 of the plugin source, you will see we’re just inserting a sodding long string into the page, and selecting the newly created element:
035: predecessor.after(template);
036: var table = predecessor.next();
I find this approach to be a lot more elegant than iterating over and over to generate every cell individually. It also cuts down the bulk considerably.
Attaching properties to the calendar
One of the things we don’t want to do is spend time keeping track of whose settings are where. If we have two or three calendars on the page, and we want their settings we want them now. To keep them close at hand all the time, we’re attaching the settings for each calendar directly to it using the data function (see Attaching data to elements in jQuery). This gives us access to them wherever we need them. If you look at line 38:
038: table.data("date", new Date(date.getTime()));
Here we’re attaching the currently active date to the calendar. A few lines down, we’re also copying the settings into it. I find this useful because not only do we avoid confusion as to what belongs to who, but we’re also helping slim down our function signatures – if everything is neatly packed, we don’t have to throw around a bunch of arguments each time.
A quick note on Dates
I’m creating a lot of new instances of Date. Since Date is a reference type, any modification to the original will mess it up wherever it’s been assigned, and vice versa. Creating a new instance is a way of telling the scripting engine not to touch the new one if the original changes.
Populating the Calendar
Filling the table with the day numbers was pleasantly simple thanks to jQuery. In line 74, we’re grabbing all the cells, iterating over them using the each function, and assigning text and styles to them based on a date. When we’re done with one cell, we increment the date (line 84) and move on to the next. Since all the cells are already in place, and we’re going over them sequentially, we don’t even have to worry about handling row changes. We just have to fill in the dates one after the other.
The styles we’re attaching here aren’t just for styling; we’re also going to use them later on to make some decisions about the cell.
The starting date is determined by taking the first day of the current month (line 16) and working backwards until we get a day that matches the first day of the week for the calendar (line 23).
The header is even simpler, since we’re just doing the same, this time using the array of day names.
The number of rows
This method of generating and populating cells may lead us to generate an excess row in cases where the current month fits exactly in 5 rows. I decided to ignore this excess and just hide it, since I know that when we come back to implement month switching, we will need a variable number of rows.
Hiding and revealing the row as needed seems preferable to deciding whether more rows are needed, creating them, removing them when they are no longer necessary, and so on.
Adding more information
So far, we have a rather boring table which doesn’t say much. Since we marked all the cells with useful information though, we can use selectors to add anything else we may need to them. In this post, we shall only add more styles to them; later on, we will also add behaviours.
Selectors are based on the CSS syntax, although they also handle a number of features which aren’t yet supported by some browsers.
In our case, we want to mark all days which do not belong in the currently active month, so that we can style them to look faded:
095: jQuery("td:not(" + currentMonthStyle + ")", table)
This reads “Select all td elements whose class does not include the value of currentMonthStyle. Only look at the descendants of the table instance, we don’t care about tds anywhere else”.
This form of coding is really neat; without that deceptively simple function, we’d have had to go over each cell, and checked it for whatever condition we were looking for. Here, we give jQuery a rough description and ask it to fetch them for us.
At line 99, we’re using the Filter method in conjunction with selectors to drill down a search. Since there isn’t, to my knowledge anyway, a CSS selector for “give me all elements which have all of classes X, Y and Z on them”, filter is the next best thing.
099: jQuery("td" + currentDayStyle, table)
100: .filter(currentMonthStyle)
101: .filter(currentYearStyle)
102: .addClass("selected");
Here we get all cells with the day class, then we look into that collection for matching month class, and on to the year until we get the set we want.
Order is important when filtering – here, we’re looking at a maximum of two elements returned by the day filter, which will be cut down to one by the month filter.
By contrast, if we filter by year first, all the cells will be returned in most cases, so the next filter will have to search in a set of 30 something elements rather than 2. While this won’t cause much of an overhead here, it’s worth keeping in mind when working with larger sets of data.
Winding Down
That’s all for this round. I was slightly surprised at how much simpler it was to write in this way; barring a few minor glitches, the hardest part was stopping myself from trying to finish the script in one sitting so I could write this post instead.
A large part of the difference is probably experience – I like to believe that I have learnt something since I wrote that script nearly five years ago. Then again, there’s no denying that the tools and frameworks we have now are far superior to whatever we had back then.
Comments? Suggestions?