“Do not wait; the time will never be ‘just right.’ Start where you stand, and work with whatever tools you may have at your command, and better tools will be found as you go along.”
— George Herbert
Recently, I needed to display a monthly calendar with a few days singled out as noteworthy. That got me to thinking, as it usually does, that it would be nice to have the empty shell calendar as a separate component so that it could be re-utilized for other potential purposes. Aside from my own unique requirements, I could see where someone might want to have something like a payroll calendar filled with holidays and paydays or an event calendar with the line-up of live music each evening. Maybe you just want to call out National Step in a Puddle and Splash Your Friends Day or you just want to be able look at a calendar and see when the cafeteria is serving chipped beef on toast with steamed carrots, Tator Tots, and a butterscotch pudding cup. I have no idea what someone might want to do with a configurable calendar, but it seems like the possibilities could be endless, so it might be worth having a reusable part.
And maybe such a part already exists. ServiceNow actually has a built-in calendar that is used quite extensively, so that seemed like a good place to start. Like many ServiceNow components, it is its own Open Source project called, appropriately enough, Full Calendar. It is quite full, indeed, and has a lot of very cool features, but I was looking for something basic and read-only, and for my purposes, I was gong to have to figure out how to turn most of those features off. I just wanted a static, single-month view that was uneditable and not interactive in any way, except for maybe the ability to click on a date and pull up a little more information. I played around with it for a while, but in the end, I decided that it was going to be more work to turn off a bunch of capabilities than it would be to start off with something much more simplistic.
So then I scoured the Interwebs for a basic HTML calendar template, of which there are many, and finally settled on this one:
It pretty much had all of the characteristics that I was looking for, so I cracked open a brand new widget and pasted in the HTML and the CSS. Then I created a new Portal Page and dragged my new widget into a full-width container and pulled it up to see how it looked. Since it is just a template, everything is hard-coded, but at this point, I just wanted to make sure that I had all of the parts and pieces and that it came out looking as it should (which it did). This is just the parts that I had found at this stage, pasted into a new Service Portal Widget.
OK, so far, so good. This was the basic structure that I wanted; now, I just needed to replace the hard-coded data with something a little more dynamic. To start with, I wanted to be able to navigate to the next month and to the previous month. Nothing beyond that — just a simple forward/backward capability that would let you move around a bit. So I tinkered with the HTML for the header and came up with this:
<header>
<span class="col-sm-4">
<button class="btn btn-default" ng-click="newMonth(-1);"><< Previous Month</button>
</span>
<span class="col-sm-4">
<h1>{{data.monthLabel}}</h1>
</span>
<span class="col-sm-4">
<button class="btn btn-default" ng-click="newMonth(1);">Next Month >></button>
</span>
</header>
This simply divided the header into three equal parts, the “go back” button, the original title (based on a variable now), and the “go forward” button. I had both buttons call the same routine, passing either a +1 or a -1, depending on which direction you wanted to go. That routine lives in the client script, which now looks like this:
function($scope, $location, spAriaFocusManager) {
var c = this;
$scope.newMonth = function(offset) {
var yy = parseInt(c.data.year);
var mm = parseInt(c.data.month) + offset;
if (mm < 0) {
mm = 11;
yy = yy - 1;
} else if (mm > 11) {
mm = 0;
yy = yy + 1;
}
var s = $location.search();
s.month = mm;
s.year = yy;
var newURL = $location.search(s);
spAriaFocusManager.navigateToLink(newURL.url());
}
}
The routine simply navigates to the same page with different values for the year and month URL parameters. To process those parameters when the page loads, we also need a little code on the server side:
var monthName = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var today = new Date();
data.month = today.getMonth();
data.year = today.getFullYear();
if ($sp.getParameter('month') && $sp.getParameter('year')) {
data.month = $sp.getParameter('month');
data.year = $sp.getParameter('year');
}
data.monthLabel = monthName[data.month] + ' ' + data.year;
This code basically establishes the values of the month and year based on the current date, and then if there is both a month and a year parameter on the URL, it overrides that initial value with the values passed via the URL. Then, using an array of month names, it establishes the calendar heading label based on the month and year values. At this point, we still have a hard-coded calendar body, but we can now use the new buttons to move forward and back through time, and the heading will change, even if the calendar itself still remains constant. All of that appears to work at this point, so now we just have to make the calendar itself as dynamic as the heading value.
To make the HTML portion simpler, I decided to build out the structure of the days to be displayed in the server side script. The structure is simply an array of 4 to 6 weeks, with each week containing an array of 7 days. The number of weeks in a month is variable, depending on how many 7-day rows it will take to display all of the days in the month, but the number of days in a week is always 7, regardless of how many of those days actually fall in the month to be displayed. The code to build out the model array is fairly self-explanatory, so I won’t dwell on that here, but here it is, for those of you interested in the details:
data.week = [];
data.firstDay = new Date(data.year, data.month, 1);
var offset = data.firstDay.getDay();
data.thisDay = new Date(data.firstDay.getTime());
while (data.thisDay.getMonth() == data.month) {
var thisWeek = [];
data.week.push(thisWeek);
for (var i=0; i<7; i++) {
var thisDay = {};
thisWeek.push(thisDay);
if (data.week.length > 1 || i >= offset) {
if (data.thisDay.getMonth() == data.month) {
thisDay.date = data.thisDay.getDate();
data.thisDay.setDate(data.thisDay.getDate() + 1);
}
}
}
}
Building the model in the server-side code makes the HTML portion quite simple. I deleted all of the hard-coded example code (except for the day of the week labels) and then replaced it with this:
<div id="calendar">
<ul class="weekdays">
<li>Sunday</li>
<li>Monday</li>
<li>Tuesday</li>
<li>Wednesday</li>
<li>Thursday</li>
<li>Friday</li>
<li>Saturday</li>
</ul>
<ul ng-repeat="w in data.week" class="days">
<li ng-repeat="d in w" class="day" ng-class="{'other-month':!d.date}">
<div class="date" ng-if="d.date">{{d.date}}</div>
</li>
</ul>
</div>
Basically, it is just a couple of ng-repeats, one for the array of weeks and another within that for the 7 days of the week. The original calendar template had numbers for all of the days, whether they were in the current month or not, but I didn’t really like that approach, so I did not calculate those values, and threw in an ng-if to keep that number DIV from appearing for those days that do not belong to the month that is the subject of the current display. At this point, we now have a completely empty calendar based on the selected month and year:
This is pretty much the empty calendar shell that I had envisioned, although at this point there is no way to pass in content for any given day. I still have to work out the best way to go about that, but conceptually, this is exactly the kind of thing that I was hoping to produce: a plain monthly calendar with moderate navigation capabilities and the potential for adding custom content based on the use case. This looks like a good stopping place for now, so I have bundled up the parts thus far and created an Update Set for anyone who might have an interest. Next time, I will add the capability to pass in content, and come up with some kind of example to demonstrate how that all works.