me

How to build a jQuery world clock in SharePoint

Posted on 9/06/2012

Update - 9/12/12

This post was deemed worthy of publishing on Nothing But SharePoint's site. May the SEO battle begin (it's a hobby to watch Google's relevancy algorithm in action).

Requirements

On a recent project, my client asked me to produce a world clock web part on his portal’s home page.  He requested a list of office locations and the time in that location, honoring Daylight Savings Time rules if applicable (timeanddate.com’s world clock page is a good working example).  The following screenshot is an example of the layout requested.

world clock web part

My first reaction was, “there must be an existing world clock web part I can use, because how would it be possible that something this common isn’t already out there?”.  So after searching for “sharepoint world clock”, I downloaded and installed the 4 most popular solutions (all server-side web parts), both community and commercial. I was shocked that none of them had this capability out of the box.  Some would have this functionality embedded with weather features, and some would output html that required serious DOM manipulation to extract the data and place it into a tabular output like I needed. When I found one that looked like a close match, there was always something that caused it not to fit, such as the lack of support for the exact time format that I needed, or not being able to override the location name after executing a web service call. Coming to the realization that I was going to have to do something custom, I set out to write my own web part because it seems simple enough, right? :)

After a bit of reflection, I arrived at the following list of requirements.

  1. No external web service dependencies.
  2. Powered by JavaScript using currently installed web parts.  Try to avoid deploying a solution with compiled code.
  3. Clocks should be based on a more reliable time source than the time on a client machine.
  4. Customized clock format (date and time).
  5. Adjusts for Daylight Savings Time when observed.

Storing the data

First of all, the question may be raised here as to why we’d want to store this data ourselves.  Won’t this make the data out of date as soon as a locality updates their Daylight Savings Time or decides, gasp, to change their GMT offset one day? In my humble opinion, the rate of change of this data is low enough to justify caching it in a list somewhere on your SharePoint farm. When you combine this with the fact that using an external web service API to fetch this information dynamically would come with a fee attached, it becomes a little easier to justify the infrequent edits required to keep your local storage up to date with the time zones of the world. (If you happen to be that edge case which produces a maintenance burden updating cached time zone and DST data, perhaps consider automating the import of the time zone database?)

So if you’re still with me, let’s do this. The first order of business is to build out the data source.  In order to store the data required to render clocks, I created a list in SharePoint to hold GMT offsets and Daylight Savings Time date ranges.  If you’d like, you can download my list template to get started with your list.  My list has the following design.

world clock list columns

I have to admit I was on the fence regarding the use of separate columns for storing date components of the Daylight Savings Time data (so I therefore reserve the right to change my mind later). The primary reason for this is because SharePoint adjusts date fields based on local time zones, even though it actually stores the date as UTC/GMT in the content database. What would be really cool here is a custom field type in SharePoint called “UTC Date” that doesn’t try do any time zone adjustments. Then, you could just use that for UI date validation and ignore the year upon retrieval when checking for Daylight Savings Time. I’ve built some custom fields types that were more involved than this, so it wouldn’t be too significant an effort, but let’s just stick with what SharePoint gives us to work with and try to keep all of our customizations away from custom Visual Studio solutions, shall we?

Retrieving the data

Creating the data viewTo make sure I can completely control the HTML output of the list data, I used the Data Form Web Part (also named data view). I prefer these over the default XSLT List View Web Part because of their concise XSLT templates. However, you can read more about the differences on Microsoft’s comparison page. To add the web part, you’ll need to use SharePoint Designer.  On a web part page, select a web part zone and choose Insert, then Data View, then Empty Data View.

After choosing my World Clock list as the data source, I just added the Title field using the Multiple Item View menu choice to initialize the data view with the proper data source.

Initializing a Data Form Web Part
Don’t worry about customizing anything on the data view at this point (unless it’s to disable paging), because we’re about to rock its world with a shiny new list of parameter bindings and a new XSLT stylesheet.

JSON FTW or: How I Learned to Stop Worrying and Love jQuery

At this point, you should switch over to the Code view so you can begin overwriting all the “less than ideal” XSLT that comes out of the box. First, let’s set up a couple of custom parameters.  In the web part’s markup, find the parameterbindings xml element, and add the following two parameter bindings.  While you’re at it, I’d recommend removing all the parameters except ListID. It tends to reappear even if you delete it, even though we don’t really need the List ID in the stylesheet. Besides, it’s already embedded in the data source, which is where it counts.
<ParameterBinding Name="Title" Location="WPProperty(Title)"/>
<ParameterBinding Name="DateTimeFormat" Location="None" DefaultValue="ddd h:mm A"/>

The Title parameter passes in the web part’s title to the clock so we can more easily control styling without having to be concerned with attempting to style the normal chrome header text (just set the Chrome to None and we’re good). However, if you’ve already customized the styling of your chrome, then have at it.  This is definitely an optional step that I decided to do.

The DateTimeFormat parameter is a format string for displaying the time. In this example, I’m using a format string compatible with the moment.js library. As you’ll see later on, however, this solution doesn’t require you to use moment.js, so you can plug in any JavaScript date library you may already be using. I decided to make the format string a parameter so it could be easily edited on the fly without requiring a new version of your js file to be published. This also means someone not very comfortable with JavaScript could update it with a lot less intimidation.

In order to pass data to jQuery, I’m going to take a separation of concerns approach to XSLT, making it responsible for as little as I possibly can, which some (of the “2 problems” persuasion) would say is the proper way to treat this technology.  Given SharePoint’s current limitation of only supporting XSLT 1.0, we have very little in the category of useful functions to help us out here anyway. Plus, XSLT has the tendency to be bloated and hard to follow, becoming a potential long-term maintenance concern.

The solution? JSON FTW!  The following stylesheet transforms the List data XML into inline JSON that jQuery will then be able to deserialize easily.  In this example, I’ve tried to remove absolutely everything out of the default stylesheet that I could get away with. For readability, I’ve left quotation marks in the JSON templates.  Although these will be replaced with &quot; after you save and close, it won’t affect the output.  That’s the not the only thing that will be modified after saving and closing, however. See those unused ddwrt namespaces?  Try and delete them and they will re-inserted back into your stylesheet after you close and reopen your page in SharePoint Designer.

<Xsl>
<xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:ddwrt2="urn:frontpage:internal">
    
    <xsl:output method="html" indent="no"/>

    <xsl:param name="Title" />
    <xsl:param name="DateTimeFormat" />
    
    <xsl:template match="/"
        xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime">
        <xsl:call-template name="clocks" />    
    </xsl:template>                                
                            
    <xsl:template name="clocks">
        <xsl:variable name="Rows" select="/dsQueryResponse/Rows/Row"/>
        
        <xsl:if test="count($Rows) &gt; 0">            
            <div id="worldClock"> 
                <div class="config">
                    <xsl:call-template name="configJSON" />            
                </div>
                <div class="header">
                    <xsl:value-of select="$Title" />
                </div>
                <div class="clocks">
                    <table>
                        <xsl:for-each select="$Rows">
                            <tr>
                                <td>
                                    <div class="title">
                                        <xsl:value-of select="@Title"/>
                                    </div>
                                </td>
                                <td>                                
                                    <div class="clock">    
                                        <xsl:call-template name="clockJSON">
                                            <xsl:with-param name="GMTOffset" select="@GMTOffset" />
                                            <xsl:with-param name="DSTGMTOffset" select="@DSTGMTOffset" />
                                            <xsl:with-param name="DSTBeginMonth" select="@DSTBeginMonth" />
                                            <xsl:with-param name="DSTBeginDay" select="@DSTBeginDay" />
                                            <xsl:with-param name="DSTBeginHour" select="@DSTBeginHour" />
                                            <xsl:with-param name="DSTBeginMinute" select="@DSTBeginMinute" />
                                            <xsl:with-param name="DSTEndMonth" select="@DSTEndMonth" />
                                            <xsl:with-param name="DSTEndDay" select="@DSTEndDay" />
                                            <xsl:with-param name="DSTEndHour" select="@DSTEndHour" />
                                            <xsl:with-param name="DSTEndMinute" select="@DSTEndMinute" />
                                        </xsl:call-template>    
                                    </div>
                                </td>
                            </tr>                        
                        </xsl:for-each>                    
                    </table>
                </div>
            </div>
        </xsl:if>    
    </xsl:template>
    
    <xsl:template name="configJSON">
        {
            "formatString" : "<xsl:value-of select="$DateTimeFormat" />"
        }
    </xsl:template>
    
    <xsl:template name="clockJSON">
        <xsl:param name="GMTOffset" />
        <xsl:param name="DSTGMTOffset" />
        <xsl:param name="DSTBeginMonth" />
        <xsl:param name="DSTBeginDay" />
        <xsl:param name="DSTBeginHour" />
        <xsl:param name="DSTBeginMinute" />
        <xsl:param name="DSTEndMonth" />
        <xsl:param name="DSTEndDay" />
        <xsl:param name="DSTEndHour" />
        <xsl:param name="DSTEndMinute" />                                                        
        {
        "gmtOffset": <xsl:value-of select="$GMTOffset" />
        <xsl:if test="string-length($DSTGMTOffset) &gt; 0
                  and string-length($DSTBeginMonth) &gt; 0
                  and string-length($DSTBeginDay) &gt; 0
                  and string-length($DSTBeginHour) &gt; 0
                  and string-length($DSTBeginMinute) &gt; 0
                  and string-length($DSTEndMonth) &gt; 0
                  and string-length($DSTEndDay) &gt; 0
                  and string-length($DSTEndHour) &gt; 0
                  and string-length($DSTEndMinute) &gt; 0">,
        "dstGmtOffset": <xsl:value-of select="$DSTGMTOffset" />,
        "dstBeginMonth": <xsl:value-of select="$DSTBeginMonth" />,
        "dstBeginDay": <xsl:value-of select="$DSTBeginDay" />,
        "dstBeginHour": <xsl:value-of select="$DSTBeginHour" />,
        "dstBeginMinute": <xsl:value-of select="$DSTBeginMinute" />,
        "dstEndMonth": <xsl:value-of select="$DSTEndMonth" />,
        "dstEndDay": <xsl:value-of select="$DSTEndDay" />,
        "dstEndHour": <xsl:value-of select="$DSTEndHour" />,
        "dstEndMinute": <xsl:value-of select="$DSTEndMinute" />
        </xsl:if>
        }
    </xsl:template>

</xsl:stylesheet>
</Xsl>

Styling the output

At this point, you should see something like this in your output:
image
Since jQuery will wait for the page to load before it will begins processing the JSON, it would probably be a good idea to hide the JSON by default.  The following CSS is one example of how we might do this.  Setting the JSON elements’ CSS style to display: none is equivalent to how jQuery implements its hide() method.  Therefore, whenever we’re ready to display the content after rendering, it will be as easy as calling show().  Also, in this example I’ve set the width of the containing table to 100%, so the web part will expand to fit whatever zone you place it in, or you can simply set the width in the web part properties.
/* world clock styles */
#worldClock {
    font-size: 8pt;
    width:100%;
}

#worldClock .config {
    display:none; /* always hide config JSON */ 
}

#worldClock .header {    
    text-align: center;    
    padding: 4px;
    font-size: 12pt;
    font-weight:bold;
}

#worldClock .clocks {
    padding: 4px;
}

#worldClock table, #worldClock td {
    width: 100%;
}

#worldClock .clock {
    display: none; /* hides JSON during rendering */
    text-align: right;
    white-space: nowrap;
}

Rendering the clocks using OO JavaScript

Now that we’ve swept all that JSON under the CSS rug, let’s get to the fun stuff: the code. What we need to do is basically iterate over each clock and replace the JSON with a formatted time.  Sounds easy, right?  Before you start ROTFLOL too soon, it’s actually a lot easier and more concise than perhaps what you’ve seen in the past when stumbling across world clock scripts that work with Daylight Savings Time. Following is all the JavaScript you’ll need in order to do this in an object-oriented fashion.
(function(){ // IIFEs protect your globals 

    $(function(){
        
        var config = $.parseJSON($("#worldClock .config").text());    
        
        // using moment.js for parsing and formatting functions
        // replace with your favorite date library if desired
        
        var parseFn = function(httpDateHeader) {
            var gmtMoment = moment.utc(httpDateHeader);
            return new Date(gmtMoment.year(), 
                            gmtMoment.month(), 
                            gmtMoment.date(), 
                            gmtMoment.hours(), 
                            gmtMoment.minutes());
        };
    
        var formatFn = function(date) {
            return moment(date).format(config.formatString);
        };
        
        var gmt = getGmtFromServer(parseFn);
        
        $("#worldClock .clock").each(function() {    
            var json = $(this).text();        
            var data = $.parseJSON(json);
            
            var clock = new Clock(data, gmt, formatFn);
                            
            $(this).text(clock.toString());
            $(this).show();
        });
    });                
        
    // issues HEAD request to the page to get date header
    // parseFn: date parse function for the RFC 822 date header
    function getGmtFromServer(parseFn) {
        var gmt;
        
        $.ajax({
            url: location.href,
            async: false,
            type: "HEAD",
            success: function(data, textStatus, jqXHR){
                gmt = parseFn(jqXHR.getResponseHeader("Date"));                            
            }
        });
        
        return gmt;
    }

    // Clock constructor
    function Clock(data, gmt, formatFn) {        
        // copy properties from deserialized JSON object
        for (prop in data)
            if (data.hasOwnProperty(prop)) this[prop] = data[prop];
        
        this.gmt = gmt;
        this.formatFn = formatFn;
    }
    
    Clock.prototype = {
        getDSTBegin : function() {
                return new Date(this.gmt.getUTCFullYear(), 
                    this.dstBeginMonth - 1, 
                    this.dstBeginDay, 
                    this.dstBeginHour, 
                    this.dstBeginMinute, 
                    0);
            },
                    
        getDSTEnd : function() {
                return new Date(this.gmt.getUTCFullYear(), 
                    this.dstEndMonth - 1, 
                    this.dstEndDay, 
                    this.dstEndHour, 
                    this.dstEndMinute, 
                    0);
            },    
    
        toStandardTime : function() {
                return (this.gmtOffset === 0)
                    ? this.gmt
                    : this.gmt.addHours(this.gmtOffset);
            },
    
        toDaylightSavingsTime : function() {
                return (this.dstGmtOffset === 0)
                    ? this.gmt
                    : this.gmt.addHours(this.dstGmtOffset);
            },
    
        toString : function(){
            var time = this.toStandardTime(); 
        
            if(typeof this.dstGmtOffset !== "undefined")
            {
                var daylightSavingsTime = this.toDaylightSavingsTime();
                
                var dstBegin = this.getDSTBegin();        
                var dstEnd = this.getDSTEnd();
        
                if(time >= dstBegin && daylightSavingsTime < dstEnd)
                    time = daylightSavingsTime;
            }
                    
            return this.formatFn(time); 
        }
    };
    
    // adds hours to a Date instance
    Date.prototype.addHours = function(hours) {
        return new Date(this.getTime() + hours * 60 * 60 * 1000);    
    };

})();

Finding a reliable GMT (They don’t make them like they used to)

Now that I’ve shown you all my cards, allow me to explain myself a bit. One of the first items for discussion is how to reliably determine the current GMT. You know you’re going about it the wrong way if you see the following line of code anywhere in your solution, which indicates you’re relying on the current time on the client’s machine (and you have no idea where that thing has been).
var date = new Date(); // client-side implementation FAIL! 

My solution was to use the time on the web server, the assumption being that the time on the server is correct.  Some may argue that this is not reliable enough, but if the clocks on your servers are off, you may likely have more significant problems (Kerberos *cough cough*) to be concerned with than just your world clock web part showing the wrong time.

Take a quick look at the getGmtFromServer() function above. This issues a HEAD AJAX call right back to the hosting page for the purposes of fetching the HTTP Date header, which just so happens to be the most convenient way to get the current time in GMT from SharePoint.  All the XSLT date functions available to us return the local time on the server, but I found no easy way of determining the server’s time zone.  Instead of making this task a much more complicated ordeal, I punted to IIS for the information I needed from the good old HTTP specification.

By the way, if you were ever searching for a good use case for the HEAD method, I would submit this one as highly appropriate. Why issue a full-blown GET request and bother the server to do all that resource rendering when you can just tell it to return what we need, which is just the headers. Unfortunately we do have to make an AJAX request to get the headers, since the browsers don’t currently trouble themselves to store the HTTP response headers anywhere after a page loads. Also, the AJAX request is a little unorthodox because I forced it to be synchronous, but I think it’s appropriate in this case (it’s an extremely lightweight request and the code is a little easier on the eyes).

Date parsing and formatting

While I’m on the topic of dates, this would be a good time to discuss the ready function.  Since JavaScript doesn’t offer extensive date parsing or formatting out of the box, you’re basically on your own to do this yourself. Since this has been a problem for a long time, by now there are probably at least a dozen different date libraries to choose from.  I initially attempted to use date.js, but for whatever reason it no longer worked correctly (It formatted midnight as a 0 instead of 12).  I then searched and found moment.js, an awesome date library that’s at least 4 years younger than date.js and smaller in size as well.  It’s capabilities far exceed my requirements and it’s available on a CDN. Winning!

Even though I may be excited about moment.js right now, maybe I’ll change my mind later, or perhaps even find some shinier date library.  This thought inspired me to write the 2 functions at the top of the ready function: parseFn for parsing the RFC 822 date header and formatFn for rendering the formatted time output. You can (and most likely will) change these functions to parse and format however you’d like without affecting how the world clock works.

Nerdy details for those so inclined to be educated (here there be dragons)

Defining functions and passing them as parameters keeps the world clock ignorant of however you decide to parse and format dates.  This is an algorithmic dependency injection technique called the strategy pattern. And as a bonus teachable moment, parseFn creates a closure by exposing the config object. The closure is how the immediate scope of Clock.toString understands what config.formatString is when it calls parseFn.

Conclusion

I’ve made every effort to make the JavaScript as readable and self-documenting as possible. I even sprinkled in some additional comments just in case you see something that may appear foreign at first glance (such as IIFEs and constructor functions). Even though this example represents a very specific set of requirements, I can imagine how using the XSLT to JSON to jQuery (XJQ?) technique could be quite effective in a lot of use cases.

I hope you’ve found this implementation a useful example on how to integrate jQuery with SharePoint. Think of it as “a way” to get JSON to jQuery, but not “the way”, as we do have other options available, such as the OData feed.

Oh yeah, and I almost forgot to tell you the specific versions of what I was running.

  • SharePoint 2010
  • jQuery 1.8.1
  • moment.js 1.7.0
Happy SharePointing!

2 comments:

  1. Do you have bead on whether this approach would be suitable for an Office365 environment, without triggering the need for sandboxing?

    ReplyDelete
  2. I haven't tried this out on an Office 365 instance, so I can't tell you for sure if the data view web part and SharePoint Designer would work (I would hope so), but if you'd rather go for a purist JavaScript approach, you should check out the OData feed that I mention above. You could skip all the steps regarding the web part entirely then, which would be awesome. If I was still doing SharePoint right now, I'd be porting this approach over to that method along with creating an app to share through the marketplace.

    ReplyDelete