Creating a custom DatePicker widget

Similar to what we’ve done creating a mobile calendar experience, with this post we’ll see how easy it is to exploit Aria Templates’ object oriented architecture to modify the look and feel of the DatePicker widget.

DatePicker is a text field able to interpret date-like user input, it can also display a drop-down calendar to let the user select a date more conveniently.
This is how the widget looks like with the default skin

 

DatePicker Widget

{@aria:DatePicker {}/}

By default the template inside the dropdown is a 3-panel calendar, but its content can be completely replaced by any custom template with the configuration paramenter {calendarTemplate}.

In this post we’ll see how to customize the widget to make it look like this

 

The calendar has the usual arrows to navigate to the previous or the next month, but also two Select widget to change month or year.

As we’ve seen in all guides, let’s create a bootstrap index.html and load a basic template.

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

        <title>Aria Templates - Custom Datepicker</title>

        <script type="text/javascript" src="aria/aria-templates-1.2.5.js"></script>
        <script type="text/javascript" src="aria/css/atskin-1.2.5.js"></script>
    </head>
    <body>

        <div id="container"></div>
        <script type="text/javascript">

            Aria.loadTemplate({
                div : "container",
                classpath : "RootTemplate"
            });

        </script>

    </body>
</html>

RootTemplate.tpl

{Template {
    $classpath : "RootTemplate"
}}
{macro main()}
{@aria:DatePicker {
    calendarTemplate : "customDatepicker.Calendar"
}/}
{/macro}
{/Template}

Now let’s create our custom template, in this first step we’ll focus only on the presentation layer.

Calendar.tpl

{Template {
    $classpath : "customDatepicker.Calendar",
    $css : ["customDatepicker.CalendarStyle"],
    $hasScript : true,
    $res : {
        dates :"aria.resources.DateRes"
    }
}}

// Shortcut variables for information in the datamodel
{var skin = data.skin/}
{var calendar = data.calendar/}
{var settings = data.settings/}

{macro main()}
    // This div widget wraps the calendar and provides borders and shadow
    {@aria:Div {
        sclass: skin.skinObject.divsclass,
        margins: "0 0 0 0",
        block: true,
        cssClass: skin.baseCSS+"general"
    }}

        {call navigation()/}
        {call calendar()/}

    {/@aria:Div}
{/macro}

/**
* Navigation header
*
* Display a block containing the arrow to navigate across months and the select widgets to choose a date.
*/

{macro navigation()}
    <div class="navigationBox">
        <span class="navigationArrow arrowLeft">
            {@aria:Icon { icon: skin.skinObject.previousPageIcon }/}
        </span>
        <span class="navigationArrow arrowRight">
            {@aria:Icon { icon: skin.skinObject.nextPageIcon }/}
        </span>

        {var monthOptions = getMonths()/}
        {var selectedMonth = calendar.months[0].firstOfMonth.getMonth()/}
        {@aria:Select {
            width : 90,
            options : monthOptions,
            value : monthOptions[selectedMonth].label
        }/}

        {var selectedYear = calendar.months[0].firstOfMonth.getFullYear()/}
        {@aria:Select {
            width : 60,
            options : getYears(),
            value : "" + selectedYear
        }/}
    </div>
{/macro}

/**
* Calendar
*
* It's the central part of the dropdown, it displays a single page calendar
*/

{macro calendar()}
    <table class="calendarTable" cellspacing="1" cellpadding="4">
        <thead>
            <tr>
            {foreach day inArray calendar.daysOfWeek}
                <th class="weekDay">${day.label}</th>
            {/foreach}
            </tr>
        </thead>
        <tbody>
        {var currentMonth = calendar.months[0] /}
        {foreach week inArray currentMonth.weeks}
            <tr>{call renderWeek(week, currentMonth.monthKey)/}</tr>
        {/foreach}
        </tbody>
    </table>
{/macro}

/**
* Render a single week, some days might overlap with previous/next month, these won't be displayed
*/

{macro renderWeek(week, monthKey)}
    {foreach day inArray week.days}
        {if day.monthKey === monthKey}
            {call renderDay(day)/}
        {else/}
            <td>&amp;nbsp;</td>
        {/if}
    {/foreach}
{/macro}

/**
* Render a single day in the calendar
*/

{macro renderDay(day)}
<td class="calendarDay ${getClasses(day)}" data-dayContainer="true">
    <span data-day="${day.jsDate.getTime()}">${day.label}</span>
</td>
{/macro}

{/Template}

its template script

CalendarScript.js

Aria.tplScriptDefinition({
    $classpath : "customDatepicker.CalendarScript",
    $dependencies : ["aria.utils.Array"],
    $prototype : {
        /**
         * Get the localized list of months in the format expected by a Select widget
         * @return {Array} list of options defined by aria.widgets.CfgBeans:ListItemCfg
         */

        getMonths : function () {
            var options = [];
            aria.utils.Array.forEach(this.dates.month, function (month) {
                options.push({
                    label : month,
                    value : month
                });
            });

            return options;
        },

        /**
         * Get the list of years (last 10) in the format expected by a Select widget
         * @return {Array} list of options defined by aria.widgets.CfgBeans:ListItemCfg
         */

        getYears : function () {
            var options = [];

            var today = new Date();
            var thisYear = today.getFullYear();
            for (var i = 0; i &lt; 10; i += 1) {
                options.push({
                    label : "" + (thisYear - i),
                    value : "" + (thisYear - i)
                });
            }

            return options;
        },

        /**
         * Get additional classes for a given day
         * @param {aria.widgets.calendar.CfgBeans.Date} day
         * @return {String}
         */

        getClasses : function (day) {
            if (day.isSelected) {
                return "selectedDay";
            }
        }
    }
});

and the CSS template

CalendarStyle.tpl.css

{CSSTemplate {
    $classpath : "customDatepicker.CalendarStyle"
}}

{macro main()}
.navigationBox {
    background : #ccc;
    border-radius : 4px;
    -webkit-border-radius : 4px;
    -moz-border-radius : 4px;
    position : relative;
    height : 24px;
}

.navigationArrow {
    position : relative;
    top : 4px;
    cursor : pointer;
}

.arrowLeft {
    float: left;
    margin-right : 15px;
}

.arrowRight {
    float : right;
}

.calendarDay {
    border : 1px solid #ccc;
    background : #efefef;
}

.calendarDay span {
    padding: 4px;
}

.dayHover {
    background : #ccc;
}

.calendarTable {
    width: 100%;
    border-spacing : 2px;
}

.selectedDay {
    background : #FBB829;
}
{/macro}

{/CSSTemplate}

This far the DatePicker is only able to display the current month, but it won’t interact to any user action.
Before proceeding lets see what events we want to react to.

Click on previous/next month arrow.
In this case the navigation is very simple as the calendar module controller provides a navigate method.
This action can be managed as simply as adding a click listener around the Icon widget and call a public method of the module controller.
Click on a specific date.
Also in this case the calendar module controller has a public method to select a date, dateClick. This method updates the data model, closes the dropdown and displays the selected day in the text field.
To get the selected date from the mouse event we use the target data object, that gives access to all node’s attributes starting with data-.
This is the reason why we have the following code in renderDay macro

<span data-day="${day.jsDate.getTime()}">${day.label}</span>
Move the mouse over or out a date in the calendar.
For styling reasons we want to give the user a visual clue that a date is click-able. This is done changing the CSS class of a date using the methods provided on a DomElementWrapper
Select a month or year from a Select widget
This interaction is more complex and requires an understanding of data model and bindings.

The way to get the value of a widget is to access the corresponding value inside the data model. We just want to be notified that the value inside the Select widget has changed, this can be done giving an onchange callback to the widget.

When the Select value changes we can read the new value from the data model and tell the module controller to navigate to a specific date still using the navigate method.

Lets see the code

Calendar.tpl

{Template {
  $classpath : "customDatepicker.Calendar",
  $css : ["sandbox.free.CalendarStyle"],
  $hasScript : true,
  $res : {
    dates :"aria.resources.DateRes"
  }
}}

// Shortcut variables for information in the datamodel
{var skin = data.skin/}
{var calendar = data.calendar/}
{var settings = data.settings/}

{macro main()}
    // This div widget wraps the calendar and provides borders and shadow
    {@aria:Div {
        sclass: skin.skinObject.divsclass,
        margins: "0 0 0 0",
        block: true,
        cssClass: skin.baseCSS+"general"
    }}

        {call navigation()/}

        {call calendar()/}

    {/@aria:Div}
{/macro}


/**
 * Navigation header
 *
 * Display a block containing the arrow to navigate across months and the select widgets to choose a date.
 */

{macro navigation()}
<div class="navigationBox">
    <span class="navigationArrow arrowLeft"
        {on click {fn: "incrementMonth", args : { increment: -1, incrementUnit: "M" }, scope : this}/}
    >
        {@aria:Icon { icon: skin.skinObject.previousPageIcon }/}
    </span>
    <span class="navigationArrow arrowRight"
        {on click {fn: "incrementMonth", args : { increment: 1, incrementUnit: "M" }, scope : this}/}
    >
        {@aria:Icon { icon: skin.skinObject.nextPageIcon }/}
    </span>

    {var monthOptions = getMonths()/}
    {var selectedMonth = calendar.months[0].firstOfMonth.getMonth()/}
    {@aria:Select {
        width : 90,
        options : monthOptions,
        value : monthOptions[selectedMonth].label,
        onchange : "changeMonthOrYear",
        bind : {
            value : {
                inside : data,
                to : "view:visibleMonth"
            }
        }
    }/}

    {var selectedYear = calendar.months[0].firstOfMonth.getFullYear()/}
    {@aria:Select {
        width : 60,
        options : getYears(),
        value : "" + selectedYear,
        onchange : "changeMonthOrYear",
        bind : {
            value : {
                inside : data,
                to : "view:visibleYear"
            }
        }
    }/}
</div>
{/macro}

/**
 * Calendar
 *
 * It's the central part of the dropdown, it displays a single page calendar
 */

{macro calendar()}
    <table class="calendarTable" cellspacing="1" cellpadding="4"
        {on click "onAction"/}
        {on mouseover {fn : "onMouseActivity", args: "enter"}/}
        {on mouseout {fn : "onMouseActivity", args: "leave"}/}
    >
        <thead>
            <tr>
                {foreach day inArray calendar.daysOfWeek}
                    <th class="weekDay">${day.label}</th>
                {/foreach}
            </tr>
        </thead>
        <tbody>
            {var currentMonth = calendar.months[0] /}
            {foreach week inArray currentMonth.weeks}
                <tr>{call renderWeek(week, currentMonth.monthKey)/}</tr>
            {/foreach}
        </tbody>
    </table>
{/macro}


/**
 * Render a single week, some days might overlap with previous/next month, these won't be displayed
 */

{macro renderWeek(week, monthKey)}
    {foreach day inArray week.days}
        {if day.monthKey === monthKey}
            {call renderDay(day)/}
        {else/}
            <td>&nbsp;</td>
        {/if}
    {/foreach}
{/macro}


/**
 * Render a single day in the calendar
 */

{macro renderDay(day)}
    <td class="calendarDay ${getClasses(day)}" data-dayContainer="true">
        <span data-day="${day.jsDate.getTime()}">${day.label}</span>
    </td>
{/macro}

{/Template}

CalendarScript.js

Aria.tplScriptDefinition({
  $classpath : "customDatepicker.CalendarScript",
  $dependencies : ["aria.utils.Array"],
  $prototype : {
    /**
     * Get the localized list of months in the format expected by a Select widget
     * @return {Array} list of options defined by aria.widgets.CfgBeans:ListItemCfg
     */

    getMonths : function () {
      var options = [];
      aria.utils.Array.forEach(this.dates.month, function (month) {
        options.push({
          label : month,
          value : month
        });
      });

      return options;
    },

    /**
     * Get the list of years (last 10) in the format expected by a Select widget
     * @return {Array} list of options defined by aria.widgets.CfgBeans:ListItemCfg
     */

    getYears : function () {
      var options = [];

      var today = new Date();
      var thisYear = today.getFullYear();
      for (var i = 0; i &lt; 10; i += 1) {
        options.push({
          label : "" + (thisYear - i),
          value : "" + (thisYear - i)
        });
      }

      return options;
    },

    /**
     * Get additional classes for a given day
     * @param {aria.widgets.calendar.CfgBeans.Date} day
     * @return {String}
     */

    getClasses : function (day) {
      if (day.isSelected) {
        return "selectedDay";
      }
    },

    /**
     * React to user action on a calendar's day. As the event comes from delegation, check that we are actually
     * clicking on a date and not somewhere else in the calendar
     * @param {aria.DomEvent} event
     */

    onAction : function (event) {
      var day = event.target.getData("day");
      if (day) {
        var jsDate = new Date(parseInt(day, 10));
        this.moduleCtrl.dateClick({
          date : jsDate
        });
      }
    },

    /**
     * React to mouseover/leave on calendar dates. This changes the style of the containing table's cell
     * @param {aria.DomEvent} event
     * @param {String} type Type of event, either "enter" or "leave"
     */

    onMouseActivity : function (event, type) {
      if (event.target.getData("day")) {
        if (type === "enter") {
          event.target.getParentWithData("dayContainer").classList.add("dayHover");
        } else {
          event.target.getParentWithData("dayContainer").classList.remove("dayHover");
        }
      }
    },

    /**
     * Callback for event listener on arrow icons. Increment/decrement a month by calling a public method of the
     * module controller interface. It also resets the datamodel of the Select widgets
     * @param {aria.DomEvent} evt
     * @param {Object} args increment factor and increment unit
     */

    incrementMonth : function (evt, args) {
      delete this.data["view:visibleMonth"];
      delete this.data["view:visibleYear"];

      this.moduleCtrl.navigate(null, args);
    },

    /**
     * Callback for onchange event on Select widget. This generates the new target date and tells the module
     * controller to navigate to that specific date
     */

    changeMonthOrYear : function () {
      var visibleMonth = 0;
      for (var i = 0; i &lt; this.dates.month.length; i += 1) {
        if (this.dates.month[i] === this.data["view:visibleMonth"]) {
          visibleMonth = i;
          break;
        }
      }

      var navigateTo = new Date(this.data["view:visibleYear"], visibleMonth, 1);
      this.moduleCtrl.navigate(null, {
        date : navigateTo
      });
    },

    /**
     * React to module event. This is especially important because we might navigate programmatically. After calling
     * .navigate the module controller raises an "update" event. We need then to refresh the view to display the new
     * visible date/month
     * @param {Object} evt module event
     */

    onModuleEvent : function (evt) {
      if (evt.name === "update") {
        this.$refresh();
      }
    }
  }
});

Download the code!

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>