Creating a mobile calendar experience

This post is in fact less about a mobile calendar widget than it is about understanding how some core parts of Aria Templates are split into reusable classes.

Aria Templates today is known for its template engine, widgets, JavaScript classes convention, module controller architecture, and some json and ajax utilities. But looking at the API doc, one immediately realizes that the number of classes present in the list is far more than these few features would normally require. Indeed, all of Aria Templates’ aspects are based on multiple classes extending from each other, delegating to each other, and interfaces implemented, etc … all in an effort to split things out by responsibility for a cleaner, more maintainable code.

All of these smaller classes are unit tested and can reliably be used independently of the more complex system they were thought as a part of originally. What I mean here is when you look at the API doc, don’t just look at the module controller or ajax utilities, the other classes are not necessarily internal gibberish stuff.

One example I would like to use to demonstrate this principle is the calendar widget.

The calendar widget, in its default atdefskin looks like that:

AT Calendar widget

But the calendar widget, even if it is today solely used through its widget statement inside templates: {@aria:Calendar {…} /} is designed on a principle that makes it reusable and extensible in a number of ways. Consider the diagram below:

AT calendar widget class diagram

The Calendar widget is a so-called template based widget because it uses the template engine internally to generate its markup. This, in turn, means that it is itself based on the MVC design pattern that AT implements:

  • M: the data model, containing in this case the list of months, weeks, days, …
  • V: the template, representing the days in month tables,
  • C: the module controller, offering public methods to the template to act on the data, or perform actions.

Looking at the calendar data model in the API doc, you see that the controller can be initialized with a few settings like minValue, maxValue, numberOfUnits, etc … So let’s try and load the controller ourselves, without using the widget at all:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Calendar</title>
    <script src="aria/aria-templates-1.1-20.js"></script>
    <script src="css/atdefskin-1.1-20.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script>
      var today = new Date();
      var inOneYear = new Date(today.getTime() + 1000 * 60 * 60 * 24 * 365);

      Aria.load({classes: ["aria.templates.ModuleCtrlFactory"], oncomplete: function () {
        aria.templates.ModuleCtrlFactory.createModuleCtrl({
          classpath: "aria.widgets.calendar.CalendarController",
          initArgs: {
            settings: {
              minValue: today,
              maxValue: inOneYear,
              startDate: today,
              displayUnit: "M",
              numberOfUnits: 12,
              firstDayOfWeek: 1,
              monthLabelFormat: "MMMM"
            }
          }
        }, function (arg) {
          // arg.moduleCtrlPrivate is the instance of the module controller
        });
      }});
    </script>
  </body>
</html>

As seen above, we make use of the aria.widgets.calendar.CalendarController class, instantiating it using the ModuleCtrlFactory, and passing it a few settings.

Now, in the callback, arg.moduleCtrlPrivate contains the instance of the module controller, initialized and ready to be used. Up to us now to assign this to our own template:

Aria.loadTemplate({
  classpath: "MobileCalendar",
  moduleCtrl: arg.moduleCtrlPrivate,
  div: "root"
});

Now, assume we want to achieve the following result: a continuous, scrollable, calendar, rather than a pages-based calendar. Something like this:

Let’s go and create the template:

{Template {
  $classpath: "MobileCalendar"
}}
  {var dayIndex = 0 /}

  {macro main()}
    <div class="calendar">
      <div class="dayNamesHeader">
        // Loop over the days of the week to display the header
        {foreach day inArray data.calendar.daysOfWeek}
          <span class="dayCell">${day.label}</span>
        {/foreach}
      </div>
      <div class="days">
        // Loop over each month, and call the renderMonth macro to take care of outputting the dates
        {for var startIndex = data.calendar.startMonthIndex, endIndex = data.calendar.endMonthIndex, index = startIndex ; index <= endIndex ; index++}
          {call renderMonth(data.calendar.months[index], index, index == startIndex, index == endIndex)/}
        {/for}
      </div>
    </div>
  {/macro}

  {macro renderMonth(month, index, isFirst, isLast)}
    // Just a few tricks to correctly output the days
    {var daysDisplayed = false /}
    {var dayOrEmptyCellDisplayed = false /}

    // This is the interesting bit, loop over the weeks
    {foreach week inArray month.weeks}
      // And then over each day in the week
      {foreach day inArray week.days}
        {if day.monthKey == month.monthKey}
          <span>${day.label}</span>
          {set daysDisplayed = true /}
          {set dayIndex += 1 /}
        {else /}
          {if isFirst && !daysDisplayed}
            <span class="dayCell"></span>
            {set dayIndex += 1 /}
          {/if}
        {/if}
        {set dayOrEmptyCellDisplayed = true /}
      {/foreach}
    {/foreach}
  {/macro}
{/Template}

This template does work but is not very useful for now. It outputs all days on a line.

In the following code snippets, we’ll use some CSS to float day cells next to each other, 7 by 7, but also some CSS classes to style the calendar nicely:

{Template {
  $classpath: "MobileCalendar",
  $css: ["MobileCalendarStyle"],
  $hasScript: true
}}

  {var dayIndex = 0 /}

  {macro main()}
    <div class="calendar">
      <div class="dayNamesHeader">
        {foreach day inArray data.calendar.daysOfWeek}
          <span class="dayCell">${day.label}</span>
        {/foreach}
      </div>
      <div class="days">
        {for var startIndex = data.calendar.startMonthIndex, endIndex = data.calendar.endMonthIndex, index = startIndex ; index <= endIndex ; index++}
          {call renderMonth(data.calendar.months[index], index, index == startIndex, index == endIndex)/}
        {/for}
      </div>
    </div>
  {/macro}

  {macro renderMonth(month, index, isFirst, isLast)}
    {var daysDisplayed = false /}
    {var dayOrEmptyCellDisplayed = false /}
    {foreach week inArray month.weeks}
      {foreach day inArray week.days}
        {if !dayOrEmptyCellDisplayed}
          <h2 class="monthLabel" style="top:${getMonthLabelTopOffset(dayIndex)}px">${month.label}</h2>
        {/if}
        {if day.monthKey == month.monthKey}
          {call displayDay(index, day) /}
          {set daysDisplayed = true /}
          {set dayIndex += 1 /}
        {else /}
          {if isFirst && !daysDisplayed}
            <span class="dayCell ${getMonthTableClassName(index)}"></span>
            {set dayIndex += 1 /}
          {/if}
        {/if}
        {set dayOrEmptyCellDisplayed = true /}
      {/foreach}
    {/foreach}
  {/macro}

  {macro displayDay(index, day)}
    <span class="${getMonthTableClassName(index)} dayCell" {on click {fn: selectDay, args: day} /}>${day.label}</span>
  {/macro}
{/Template}

Here is the template script, used to return the month labels and the class names:

Aria.tplScriptDefinition({
  $classpath: "MobileCalendarScript",
  $prototype: {
    getMonthTableClassName: function(index) {
      return index % 2 ? "monthEven" : "monthOdd";
    },
    getMonthLabelTopOffset: function(index) {
      var cellHeight = 40;
      return cellHeight*2 + (index / 7 * cellHeight);
    }
  }
});

And finally the CSS template:

{CSSTemplate {
  $classpath: "MobileCalendarStyle"
}}
  {macro main()}
    .calendar {
      width: 280px;
      overflow: hidden;
      padding-left: 30px;
      background: #666;
      {call getCrossBrowserProperty("user-select", "none") /}
    }
    .dayNamesHeader {
      background: #666;
      color: white;
      overflow: hidden;
    }
    .days {
      clear: left;
    }
    .dayCell {
      display: block;
      float: left;
      font-size: 20px;
      text-align: center;
      line-height: 40px;
      width: 40px;
      height: 40px;
    }
    .dayCell:hover {
      background: #333;
      color: white;
    }
    .monthEven {
      background: #eee;
    }
    .monthOdd {
      color: #555;
      background: #fff;
    }
    .monthLabel {
      margin: 0;
      padding: 0;
      font-size: 20px;
      position: absolute;
      left: -115px;
      width: 250px;
      text-align: center;
      color: #fff;
      font-weight: normal;
      {call getCrossBrowserProperty("transform", "rotate(-90deg)") /}
      {if aria.core.Browser.isIE7 || aria.core.Browser.isIE8}
        filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
        left: 0;
      {/if}
    }
  {/macro}

  {macro getCrossBrowserProperty(name, value)}
    {if aria.core.Browser.isSafari || aria.core.Browser.isChrome}
      -webkit-${name}: ${value};
    {elseif aria.core.Browser.isFirefox /}
      -moz-${name}: ${value};
    {elseif aria.core.Browser.isOpera /}
      -o-${name}: ${value};
    {elseif aria.core.Browser.isIE9 /}
      -ms-${name}: ${value};
    {/if}
    ${name}: ${value};
  {/macro}
{/CSSTemplate}

2 responses on “Creating a mobile calendar experience

  1. Nicolas A Team Member
    on 15 March 2012, 3:52 pm

    Really nice example! Helped me a lot achieving a nice calendar component for one of my project. Furthermore the development was very quick (a matter of hours). Keep up the great job :)
    Travel Seeker mobile calendar

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>