Build a Better Salesforce Lightning Datepicker

Salesforce Lightning is a great tool for building modular, extensible and modern UIs on the Salesforce platform.

Salesforce Lightning Datepicker

There are a lot of pre-built UI controls that you can use to create an interface in Salesforce, but there are a couple of vital missing pieces.

The first of these is the Lookup component, which I wrote about in a previous post, “Embed a Lightning Component in a Visualforce Pagecur”. The second is a good, Salesforce Lightning Design System styled Datepicker component. There is an existing component, but it is not lightning styled and integrating it into a Lightning App often causes style problems.

Salesforce have defined what they think a picker should look like on their extensive Salesforce Lightning Design System style site, which contains extensive resources to help developers create beautiful UIs – you can see their definition here.

What I am going to show is how to create your own Lightning Datepicker component.

Note: This datepicker has been configured for U.S.-based locales. For international use, you may need to do your own customization.

Background

To build this component, I used several sources – however, as the result is kind of a mashup, I haven’t included an licence information, as most of the code is my own.

  • I used some of the structure of the existing Aura lightning component here.
  • I also used some data structures and ideas from here.

Finally, I was forced to use moment.js despite trying not to have any external dependencies, but due to a bug in the Lightning framework – (the $A.localizationService.parseDateTime() method seems to be blocked by the Locker Service), I was forced to use it for string to date conversions.

Get moment here.

<aura:event type="COMPONENT" description="Dispatched when a date cell is clicked" />

The date change event – the parameter is a string containing the value of the selected date.

<aura:event type="COMPONENT" description="Despatched when the date is changed in the DatePicker component" >
  <aura:attribute name="value" type="String" description="string value of the selected date" access="global" />
</aura:event>

Next, you need to define a “DateCell” component – this represents a single day in the date grid. As you can see, the DateCellClick event is registered as an event that can be dispatched by this component.

<aura:component >
    <aura:attribute name="ariaSelected" type="String" default="false" description="Highlight this control."/>
    <aura:attribute name="ariaDisabled" type="String" default="false" description="Disable this control."/>
    <aura:attribute name="tabIndex" type="Integer" default="-1" description="The tab index of the anchor element."/>
    <aura:attribute name="value" type="Date" description="The date this component renders."/>
    <aura:attribute name="label" type="String"/>
    <aura:attribute name="tdClass" type="String"/>
    <td class="{!v.tdClass}" role="gridcell" aria-selected="{!v.ariaSelected}" aria-disabled="{!v.ariaDisabled}" onclick="{!c.handleClick}" >
       <span class="slds-day" >{!v.label}</span>
    </td>
    <aura:registerevent name="dateCellClick" type="c:DateCellClick" description="click event" />
</aura:component>

For this component, I use a simple controller, which refires a standard “onclick” event as a Lightning DateCellClick event.

({
    handleClick : function(component, event, helper) {
        var click = component.getEvent("dateCellClick");
        click.fire();
    }
})

Now we can define the main component. This component uses pre-rendered cells (like the original Aura DateGrid), which means it’s easy to identify individual cells and render the day values before and after the current month.

There are two main parts to the grid. First a simple ui:inputText:

<div class="slds-form-element ">
  <label class="slds-form-element__label" for="closedate">{!v.label}</label>
  <div class="slds-form-element__control">
    <div class="slds-input-has-icon slds-input-has-icon--right">
      <c:SVG class="slds-input__icon slds-icon-text-default icon--large" xlinkHref="{!$Resource.SLDS105 + '/assets/icons/standard-sprite/svg/symbols.svg#event'}"></c:SVG>
      <ui:inputDate aura:id="dateInput" class="slds-input" value="{!v.value}" format="{!v.formatSpecifier}" displayDatePicker="false" updateOn="keyup" focus="{!c.handleInputFocus}" />
    </div>
  </div>
</div>

This code holds the date and formats it for output.

Next, there is the grid:

<div aura:id="grid" class="slds-datepicker slds-dropdown slds-dropdown--left slds-hide" onmouseleave="{!c.handleGridMouseLeave}" onmouseenter="{!c.handleGridMouseEnter}">
   <div class="slds-datepicker__filter slds-grid">
     <div class="slds-datepicker__filter--month slds-grid slds-grid--align-spread slds-grow">
       <div class="slds-align-middle">
         <a onclick="{!c.goToPreviousMonth}" href="javascript:void(0);" class="slds-button slds-button--icon-container">
           <c:SVG class="slds-button__icon slds-button__icon--small slds-m-top--small" xlinkHref="{!$Resource.SLDS105 + '/assets/icons/utility-sprite/svg/symbols.svg#left'}">
           </c:SVG>
         </a>
       </div>
       <h2 id="month" class="slds-align-middle" aria-live="assertive" aria-atomic="true">{!v.monthName}</h2>
       <div class="slds-align-middle">
         <a onclick="{!c.goToNextMonth}" href="javascript:void(0);" class="slds-button slds-button--icon-container">
           <c:SVG class="slds-button__icon slds-button__icon--small slds-m-top--small" xlinkHref="{!$Resource.SLDS105 + '/assets/icons/utility-sprite/svg/symbols.svg#right'}">
           </c:SVG>
         </a>
       </div>
     </div>
     <div class="slds-shrink-none">
       <ui:inputSelect aura:id="yearSelect" class="slds-select spear-select" label="pick a year" labelClass="slds-assistive-text" required="false" change="{!c.handleYearChange}" />
     </div>
   </div>
   <table aura:id="maintable" class="datepicker__month" role="grid" aria-labelledby="month">
     <thead>
       <tr id="weekdays">
         <aura:iteration items="{!v._namesOfWeekdays}" var="day">
           <th scope="col" class="dayOfWeek">
             <abbr title="{!day.shortName}">{!day.shortName}</abbr>
           </th>
         </aura:iteration>
       </tr>
     </thead>
     <tbody>
       <tr aura:id="week1">
         <c:DateCell aura:id="0" />
         <c:DateCell aura:id="1" />
         <c:DateCell aura:id="2" />
         <c:DateCell aura:id="3" />
         <c:DateCell aura:id="4" />
         <c:DateCell aura:id="5" />
         <c:DateCell aura:id="6" />
       </tr>
       <tr aura:id="week2">
         <c:DateCell aura:id="7" />
         <c:DateCell aura:id="8" />
         <c:DateCell aura:id="9" />
         <c:DateCell aura:id="10" />
         <c:DateCell aura:id="11" />
         <c:DateCell aura:id="12" />
         <c:DateCell aura:id="13" />
       </tr>
       <tr aura:id="week3">
         <c:DateCell aura:id="14" />
         <c:DateCell aura:id="15" />
         <c:DateCell aura:id="16" />
         <c:DateCell aura:id="17" />
         <c:DateCell aura:id="18" />
         <c:DateCell aura:id="19" />
         <c:DateCell aura:id="20" />
       </tr>
       <tr aura:id="week4">
         <c:DateCell aura:id="21" />
         <c:DateCell aura:id="22" />
         <c:DateCell aura:id="23" />
         <c:DateCell aura:id="24" />
         <c:DateCell aura:id="25" />
         <c:DateCell aura:id="26" />
         <c:DateCell aura:id="27" />
       </tr>
       <tr aura:id="week5">
         <c:DateCell aura:id="28" />
         <c:DateCell aura:id="29" />
         <c:DateCell aura:id="30" />
         <c:DateCell aura:id="31" />
         <c:DateCell aura:id="32" />
         <c:DateCell aura:id="33" />
         <c:DateCell aura:id="34" />
       </tr>
       <tr aura:id="week6">
         <c:DateCell aura:id="35" />
         <c:DateCell aura:id="36" />
         <c:DateCell aura:id="37" />
         <c:DateCell aura:id="38" />
         <c:DateCell aura:id="39" />
         <c:DateCell aura:id="40" />
         <c:DateCell aura:id="41" />
       </tr>
       <tr>
         <td colspan="7" role="gridcell"><a onclick="{!c.goToToday}" href="javascript:void(0);" class="slds-show--inline-block slds-p-bottom--x-small">Today</a></td>
       </tr>
     </tbody>
   </table>
 </div>

The code just above the actual grid cells is taken directly from the Lightning CSS guide – however, I was forced to use an anchor tag enclosing the SVG element, as using a regular button seemed to create an unhandled click event that caused a page refresh.

I wasn’t able to catch that event and stop its propagation, so the anchor tag was the next best thing. Anyone who knows how to do this, please contact me.

The controller contains some basic logic but hands most of the work to the handler. To be honest, I’m finding it hard to work out whether to put code in the controller or the handler. So far, I’ve got a bit of both :).

The is some init code, which formats the date string supplied as a parameter and builds the grid. The other code handles mouse events to prevent the grid from closing when active.

Finally, there are some handers for the various UI events that occur – clicking back, forward, selecting a year and selecting a day.

As soon as the A.localeHandler bug is fixed, I’ll remove the moment dependency.

Here it is:

({
  doInit: function(component, event, helper) {
    for (var i = 0; i < 41; i++) {
      var cellCmp = component.find(i);
      if (cellCmp) {
        cellCmp.addHandler("dateCellClick", component, "c.handleClick");
      }
    }
    var format = component.get("v.formatSpecifier");
    var datestr = component.get("v.value");
    var langLocale = $A.get("$Locale.langLocale");
    var momentDate = moment(datestr, 'MM/DD/YYYY');
    var currentDate;
    if(currentDate == null || !currentDate.isValid()){
      currentDate = moment().toDate();
    }
    else {
      currentDate = momentDate.toDate();
    }
    helper.setDateValues(component, currentDate, currentDate.getDate());
    // Set the first day of week
    helper.updateNameOfWeekDays(component);
    helper.generateYearOptions(component,currentDate);
    var setFocus = component.get("v.setFocus");
    if (!setFocus) {
      component.set("v._setFocus", false);
    }
    helper.renderGrid(component);
  },
  handleInputFocus: function(component, event) {
    var grid = component.find("grid");
    if (grid) {
      $A.util.removeClass(grid, 'slds-hide');
      $A.util.removeClass(grid, 'slds-transition-hide');
    }
  },
  handleGridMouseLeave: function(component, event) {
    var grid = component.find('grid');
    if (grid) {
      $A.util.addClass(grid, "slds-transition-hide");
    }
    var timeout = window.setTimeout(
      $A.getCallback(function() {
        if (grid.isValid()) {
          $A.util.addClass(grid, "slds-hide");
        }
      }), 2000
    );
    component.set("v._windowTimeout", timeout);
 
  },
  handleGridMouseEnter: function(component, event) {
    var grid = component.find('grid');
    if (grid) {
      $A.util.removeClass(grid, 'slds-hide');
      $A.util.removeClass(grid, 'slds-transition-hide');
    }
    var timeout = component.get("v._windowTimeout");
    if (timeout){
      clearTimeout(timeout);
    }
  },
  handleYearChange : function (component, event, helper){
    var newYear = component.find("yearSelect").get("v.value");
    var date = component.get("v.date");
    helper.changeYear(component,newYear,date);
  },
  handleClick: function(component, event, helper) {
    console.log('Date picker controller click' + event);
    helper.selectDate(component, event);
  },
  goToToday: function(component, event, helper) {
    event.stopPropagation();
    helper.goToToday(component, event);
    return false;
  },
  goToPreviousMonth: function(component,event,helper) {
    event.stopPropagation();
    helper.changeMonth(component, -1 );
    return false;
  },
  goToNextMonth: function(component,event,helper) {
    event.stopPropagation();
    helper.changeMonth(component, 1);
    return false;
  }
});

Finally, the helper does some more low-level rendering and element manipulation.

Using the picker in your form

Assuming you are using an Opportunity (obviously any object with a date would work), define the Opportunity as an attribute on your component:

<aura:attribute name="opportunity" type="Opportunity" default="{ 'sobjectType': 'Opportunity',
                             'Name': 'New Opportunity',
                             'StageName': 'Some Stage' />
<aura:handler name="dateChangeEvent" event="c:DateChange" action="{!c.handleDateChange}" />

In your form, put the below(once you have created it) as a top level member of the form:

<div class="slds-form-element slds-m-top--medium">
    <c:DatePicker aura:id="closeDate" label="Close Date" placeholder="Enter a Date" value="{!v.opportunity.CloseDate}" formatSpecifier="MM/dd/yyyy" />
</div>

Finally, in your controller or helper update your date:

handleDateChange: function(cmp, event, helper) {
  var dateSelector = event.getSource().getLocalId();
  if (dateSelector == 'closeDate'){
    var opp = cmp.get("v.opportunity");
    opp.CloseDate = event.getParam("value");
  }
}

It should all just work!

Table Sample
Final Results

Get the DatePicker files

All the files are located on Github.

Enjoy!

Moving Forward in Lightning

Have questions? Let us know in a comment below, or contact our team directly. You can also check out our other posts on customizing your Salesforce solution.

41 thoughts on “Build a Better Salesforce Lightning Datepicker”

  1. Hi Caspar, regarding the button issue. If the datepicker is in a form, the button will submit the form which would look like a page refresh. The implicit type on a form button is type=”submit” you could try type=”button” (or, since this is Lightning probably use ). Additionally, you can cancel the submit of a form by returning false onsubmit or onclick of the button.

    1. Hi Ben, the main problem is that because Salesforce controls all events, making an input of type=”button” submits the form. If this were a regular webpage, I could return false on the click handler – however, it seems that by the time I get access the event, it’s too late to cancel it. I’ve tried returning false, but so far to no avail.

  2. Hi Caspar, Thanks for you code. its helped alot. i have one issue with this data picker. My date is displaying in textbox as date -1 day. eg: if i select 9/15/2016 then it is displaying as 9/14/2016. But if i show selected value in label then it is showing as 2016-9-15. i guess this issue might be with locale. Please help me to fix this issue.

  3. Caspar, I am having the same issue as Satish even with my locale set to US. The datepicker seems to work fine except for that it highlights the selected date-1. Could this be a timezone issue?

    1. Hi Zachary, I think I’ve resolved the issue – please check the github repository in the blog post. I think the problem was solved by using a UTC date rather than a regular date.

  4. Very cool!! Thanks so much for this. Drives me nutz we are having to code hundreds if not thousands of lines of code just for basic UI controls and having to explain to the customer while something simple is taking so long. I get LDS is a ‘design framework’ but certain things are useless with coding hundreds of lines of custom code.

    1. Yes, it does seem that a relatively small amount of effort by Salesforce could help out a lot of developers. Glad you like it!

  5. Brian Mansfield

    Caspar, thank you so much for this contribution – I implemented the version you have on Git today, and it looks amazing. Wonderfully designed!

    1. I’m happy that it’s been so useful! Let me know if there are any bugs via the github interface and I’ll see if I can fix em.

  6. I actually used a lot of what you had and rebuilt it into angularjs / angular bootstrap ui datepicker with lds styles on force.com sites…. it wasn’t easy though. 🙂 And now I’m actually using this to put in our backend / classic service cloud console lightning component which is embedded in a vf page using lightning out.. Why does it always seem like I get the edge cases that have no love with the lightning ui components. 🙂 this stuff would be so easy if I was in a legit lightning container..ever… 🙂

    1. Glad it was useful! Yeah, I imagine you’d have to do quite a bit of work to convert to angular – you could do things like dynamically generate the day divs (which are static so they can be referenced by aura:id) – make sure you have a look at the most recent iteration – I keep adding features that could be handy… of course then they would need to be Angularized… sigh 🙂

  7. One question on the select component. (actual datepicker).. from a lightningout perspective.. the default is to display on the bottom left of the input which makes sense in most cases but again.. for mine it doesn’t. A lot of date pickers such as bootstrap have positioning options using options and settings such as popup-placement:xxxxx ….obviously that might be overkill since most people will want it there but it looks like this is just positioning itself within the div below without any say.. absolute positioning. Correct?

    I could potential see an issue for me as this would have to sit in the right of the label div on top so I may have to do some absolute positioning. I’m honestly afraid to do anything out of the ordinary in these locker service days as I feel like that thing just beats me down any time I want to do something custom.

    Just wanted to make sure I wasn’t missing some hooks available?

    1. Yes you are correct – it needs some logic there. The logic for this is a little tricky, which is why I left it out of the initial picker. Maybe I’ll have a got at making that sort of option available, but for now, unfortunately, you are going to have to implement this one on your own. If you do, I’d really like to see what you do – you could even make a pull request to the github project. Don’t be to worried about Locker Service – the secure versions of most html elements are very close to real html element attribute parity. Since the date picker hides and shows it’s own datepicker grid, it has full control of it’s html attributes – it should be fine to position the grid as you see fit. Good luck!!

  8. Will do. I may just do a quick hack as all this work is really just to add no value to the client due to locker service fun ……so he’s not too happy about having to pay for this. I have a few things that are working now but I’m not sure they are really ready for the wild. at the very least i’ll get back to you with code snippets and maybe do a pr. I’m also having fun with lightning:inputRichText.. We all know SFDCs glorious history on RTE’s and this one is not making me feel any better. 🙂 I will say quill isn’t bad in the angular environment but this sfdc lightning RTE component leaves A LOT to be desired.

    1. Yeah, I haven’t tried to get a rich text editor working in Lightning… can’t be fun :). I wonder if a solution for drop-down location could be just to make the dropdown bit into a centered modal which will guarantee that it won’t disappear off the screen. Locker Service definitely is like that guy that comes to your party that you think is really cool but then you realize he’s a gang associate and had just called 25 of his best gang buddies to come hang with you.

  9. A first draft… really raw and again. somewhat of a hack…. May have to adjust the css for your own usage pending viewport. We cut the size of the datepicker in 1/2 vss and removing the year select and today link.. (not shown below) so that may matter as well in re to the positioning.

    And you are being kind about Mr. Locker Service…

    ..
    ..
    ..


    /* @todo RJN There has to be a better way then the below. css geeks.. chime in!
    *
    */
    .THIS.custom-datepicker–top {
    position:absolute;
    bottom: 30px;
    }


    doInit: function(component, event, helper) {

    for (var i = 0; i < 41; i++) {
    var cellCmp = component.find(i);
    if (cellCmp) {
    cellCmp.addHandler("dateCellClick", component, "c.handleClick");
    }
    }

    var format = component.get("v.formatSpecifier");
    var datestr = component.get("v.value");

    //— RJN Custom
    console.log('auradatepicker — doInit. datestr='+datestr);
    var position = component.get("v.position");
    var useNubbin = component.get("v.useNubbin");
    helper.positionDatepicker(component, position, useNubbin);
    //—
    ..
    ..
    ..


    /**
    * RJN Custom add
    * Add the appropriate css class based on position param. native lds classes used and 1 custom class for
    * abs positioning as sfdc doesn’t really do a ‘top’ positioning the way I want it.
    *
    * @param component
    * @param position – [bottom-left,bottom-right,top-left,or top-right] currently supported. bottom-left the default
    * @param useNubbin – true if we want to show an LDS nubbin, otherwise false by default.
    */
    positionDatepicker: function(component, position, useNubbin) {
    var cGrid = component.find(‘grid’);

    var sldsRight = ‘slds-dropdown–right’;
    var sldsLeft = ‘slds-dropdown–left’;
    var customTop = ‘custom-datepicker–top’;

    //— I know the below seems back-asswards…
    var sldsNubbinTopRight = ‘slds-nubbin–bottom-right’;
    var sldsNubbinTopLeft = ‘slds-nubbin–bottom-left’;
    var sldsNubbinBottomLeft = ‘slds-nubbin–top-left’;
    var sldsNubbinBottomRight = ‘slds-nubbin–top-right’;

    switch(position) {
    case ‘bottom-left’:
    $A.util.addClass( cGrid,sldsLeft );
    if(useNubbin) { $A.util.addClass( cGrid,sldsNubbinBottomLeft ); }
    break;

    case ‘bottom-right’:
    $A.util.addClass( cGrid,sldsRight );
    if(useNubbin) { $A.util.addClass( cGrid,sldsNubbinBottomRight ); }
    break;

    case ‘top-left’:
    $A.util.addClass( cGrid,sldsLeft );
    $A.util.addClass( cGrid,customTop );
    if(useNubbin) { $A.util.addClass( cGrid,sldsNubbinTopLeft ); }
    break;

    case ‘top-right’:
    $A.util.addClass( cGrid,sldsRight );
    $A.util.addClass( cGrid,customTop );
    if(useNubbin) { $A.util.addClass( cGrid,sldsNubbinTopRight ); }
    break;
    default:
    $A.util.addClass( cGrid,sldsLeft );
    }
    },

  10. Oops damn RTEs well that didn’t work as it cut out my usage portion and the component portion.. must have taken my html comments and then nuked some of the rest.. i’ll take a new pr at some point or email you directly?? as it’s hard to put code in this RTE.. If it’s possible and you can get rid of that code snipped I’d appreciate it as it’s incomplete.

  11. Why would I be getting on the preview of the test App?

    This page has an error. You might just need to refresh it.
    Action failed: c:DateCell$controller$handleClick [Cannot read property ‘fire’ of null]
    Failing descriptor: {c:DateCell$controller$handleClick}

    1. Are you handling the lookupChangeEvent? If you are having issues, make sure you get the latest source from github – I’m pretty sure it updates correctly from that source.

    1. I’m handling that here in the inputDate dataChangeEvent:

      Check my github repo – I update it periodically.

    1. Yes, you could do that. In the generateMonth method, in the DatePickerHelper, somewhere around here:

      if (d.getMonth() === month - 1 || d.getFullYear() === year - 1) {
      cellCmp.set("v.ariaDisabled", "true");
      tdClass = 'slds-disabled-text';
      } else if (d.getMonth() === month + 1 || d.getFullYear() === year + 1) {
      cellCmp.set("v.ariaDisabled", "true");
      tdClass = 'slds-disabled-text';
      }

      You can see that certain classes are being disabled. You’d have to evaluate weather the current cell was in the future, and set it disabled if it wasn’t.

  12. How would I be able to get this to work for multiple dates? Like 2 of the date pickers that return different dates inside an Aura:iteration

    1. Well, I suppose the main thing would be to identify which picker the event came from. For each picker, you could have a different event handler function, which would get you the correct field… then you’d need to know which iteration, which you might need to handle by passing in the iteration record id to the date picker and passing it back in the datechange event.
      It’s very possible to do this, but remember, date pickers are pretty heavy and you might want to consider lazy loading each picker too (ie instantiate the picker when the user clicks into the field, because if you render 20+ date pickers, you’ll have a noticeable lag when the component loads. (I’ve done the lazy load thing before, but not more than one picker in any iteration). Good luck!

  13. On click/selection of date in the grid/cell this error is displayed….

    This page has an error. You might just need to refresh it.
    Action failed: c:DateCell$controller$handleClick [clientAction.$runDeprecated$ is not a function. (In ‘clientAction.$runDeprecated$(event)’, ‘clientAction.$runDeprecated$’ is undefined)]
    Failing descriptor: {c:DateCell$controller$handleClick}

    need help…….

    1. As you can see, $run has been deprecated. I suggest you try to find out what replaces it. I don’t think it’s a difficult fix.

  14. amerbearattest

    I get this error when i click on and date
    Uncaught Assertion Failed!: no client action by name c.handleClick : false

    it seems to be that cellCmp.addHandler(“dateCellClick”, component, “c.handleClick”); in the DatePickerController it cant read/access “c.handleClick” in the controller on click

    1. I will shortly be looking at the datepicker to see if this and a couple of other bugs can be fixed – I’ll let you know when I’m done.

    1. No sorry, that would be entirely new functionality. You are welcome to build this yourself – if you do, please submit a pull request. Thanks!

  15. Hey, does this work with mobile? Need to make sure that the native device date picker isn’t used on mobile and would need this to override it.

    1. Unfortunately, I haven’t built this picker to handle mobile… Maybe one day – although I’d likely re-implement in lwc.

        1. I have made a start actually, but currently I’m trying to decide about what to do about the lack of a good built in datetime conversion library in LWC. When I’ve figured that out, I’ll give it a go.

Leave a Comment

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

We're celebrating 20 years! Read about our journey here.

Party horn and confetti
Scroll to Top