Convert a JavaScript Datepicker to a Lightning Web Component

Hello there developer! Now you can leverage Lightning Web Components on multiple platforms, in addition to the Salesforce platform. Whether your background is in JavaScript, Salesforce, or another platform, here is how you can use the Lightning Web Components programming model to create a datepicker web component.

Why create a web component? They’re reusable across the web on different platforms, and testable using open source frameworks such as Jest. Here is the original JavaScipt component and the revised component.

Revisit the component’s design

Migrating a component is not a line-by-line conversion, and it’s a good opportunity to revisit your component’s design.

We make the following changes in the Lightning web component:

  1. Since the component uses plenty of date manipulation, use momentjs library to easily format dates.
  2. Allow the user to select a date by clicking on a date cell. The selected date should be highlighted in the UI.
  3. Add a Today button: on user click, go to Today and update the month and year. Highlight today’s date cell in the UI.
  4. The original component used HTML injection (line 136) but that is highly discouraged for security reasons. Instead, let’s create date nodes and append them to the DOM.
  5. The original component uses a table element to append date nodes to. This is costly because the table element needs many DOM nodes, including table header, table rows, and table cells. Additionally, table elements are difficult to display in a responsive way. Instead, let’s use an unordered list, with list elements for date nodes. Additionally, since table headers are static, we extract them to the markup as list elements in a separate unordered list.

With the requirements defined, let’s start the migration by creating a Lightning Web Components app.

First, create a new Lightning Web Components app

Start by creating an LWC app. Then create a datepicker folder at the same level as the app folder. Inside the datepicker folder, create the files datePicker.css, datePicker.html, and datePicker.js. As a best practice let’s add a __tests__ folder too, to add tests later. The resulting src folder structure looks like this.

Lastly, we change the app.html to use the new date picker component. Notice how the child component becomes my-date-picker due to the folder structure and the camel case folder name.

<template>
    <my-date-picker></my-date-picker>
</template>

Save app.html, start the server via npm run watch, and watch the browser reload automatically! Live reloading makes development fast and fun.

Migrate markup

The original markup uses the id attribute. This is discouraged in LWC, since ID values may be transformed into globally unique values on template render. Instead, use the element’s class attribute. As a side note, you can still use IDs for ARIA attributes. For our use case, let’s convert all IDs to classes.

Instead of:

  <button id="btnPrev" type="button">Prev</button>
  <button id="btnNext" type="button">Next</button>

We have:

<button class="prev" onclick={previousMonth} type="button">Prev</button>
<button class="next" onclick={nextMonth} type="button">Next</button>

Notice the converted markup also includes event handlers, referring to methods in the LWC component. In the original calendar picker the event handlers are added via JavaScript.

Instead of the div element, we use an unordered list to append the date nodes to. Hence we replace <div id="divCal"></div> with <ul class="datePickerHolder" lwc:dom="manual"></ul>. Notice we marked our new container element with the attribute lwc:dom="manual", to later append DOM elements to it.

The final Lightning web component HTML file has a container with four child nodes:

  1. A <div class="buttonContainer"> for the Today button, selected date, and year
  2. Another <div class="buttonContainer"> to navigate to Previous and Next month
  3. <ul class="header"> for the seven days of the week
  4. <ul class="datePickerHolder" lwc:dom="manual"></ul> to attach the date nodes to.

Migrate CSS

Lightning web components use standard CSS syntax. All we have to do is replace the CSS rules that use ID selectors and use class selectors. Here is the finished CSS file based on the new markup.

Migrate JavaScript

The JavaScript file for Lightning Web Components differs drastically from the original component. Let’s take it step-by-step:

  1. Declare the properties.
  2. Port over window.onload code.
  3. Move event handler methods.
  4. Define getters to format the properties.
  5. Refactor rendering code.

Before we write any code, what does the default datepicker.js look like? The first line is an import statement. The core module in Lightning Web Components is lwc. The import statement imports LightningElement from the lwc module. LightningElement is a custom wrapper of the standard HTML element.

import { LightningElement } from 'lwc';

Next, notice datepicker.js uses the ES6 class syntax. class in JavaScript is a syntactic sugar over prototypes.

export default class DatePicker extends LightningElement {}

Declare properties

In the original component, these properties are defined with today’s date:

this.currMonth = d.getMonth();
this.currYear = d.getFullYear();
this.currDay = d.getDate();

In the LWC counterpart, today becomes a constant since its value does not change throughout the component lifecycle. We use momentjs as a helper for date-handling so new Date() becomes moment().

Here are all of the properties of the new datepicker LWC component, commented with their use case.

isDatePickerInitialized; // ensure the datepicker is initialized only once
@track dateContext = moment(); // the displayed month; changes with user interaction
@track selectedDate = moment(); // the user-selected date
@track error; // if the date holder element is not rendered, set the error
lastClass; // on select date, revert the class of the previously selected date node

Notice dateContext, selectedDate, and error are decorated with @track. The @track decorator in Lightning Web Component makes the property reactive: when the value of the property changes, the component rerenders.

Port over window.onLoad code

The original component’s JavaScript initialized the component in window.onload. This is where we call a method (ie. c.showcurr) to create and attach date nodes.

window.onload = function() {

  // Start calendar
  var c = new Cal("divCal");            
  c.showcurr();
  ...
}

In the Lightning web component we also need to initialize the datepicker after the DOM is loaded. Let’s use renderedCallback, one of LWC’s powerful lifecycle hooks, because the container node should be queryable then. Since renderedCallback is called after every component render, we use the property isDatePickerInitialized to gate the initial render of the component.

renderedCallback() {
    if (this.isDatePickerInitialized) {
        return;
    }

    this.isDatePickerInitialized = true;
    this.refreshDateNodes();
}

Move event handler methods

Within the component definition, both the JavaScript component and the LWC component refer to instance methods as this.methodName. Hence the original component’s event handler method c.previousMonth on line 42 easily maps into LWC’s this.previousMonth. Let’s move over this.nextMonth too.

Upon clicking the Today button, the datepicker should update to show the current month, with today selected. We implement a this.goToday method to to fulfill the requirement.

goToday() {
    this.selectedDate = today;
    this.dateContext = today;
    this.refreshDateNodes();
}

Notice the event handler method this.setSelected is decorated with the @api decorator. The @api exposes the method as public. This enables a parent component to call the setSelected method to set a date.

@api
setSelected(e) {
    const selectedDate = this.template.querySelector('.selected');
    if (selectedDate) {
        selectedDate.className = this.lastClass;
    }

    // use destructuring to get e.target.dataset.date
    const {date} = e.target.dataset;
    this.selectedDate = moment(date);
    this.dateContext = moment(date);
    this.lastClass = e.target.className;
    e.target.className = 'selected';
}

Define getters to format properties

To format the date properties in the UI, use getter methods to return formatted year, month, and selectedDate. A getter method looks like this. In this case the getter returns a formatted date string.

get formattedSelectedDate() {
    return this.selectedDate.format('YYYY-MM-DD');
}

To include the string in the DOM, reference the getter name, for example on line 9:

<p>{formattedSelectedDate}</p>

Refactor rendering code

In the original component, we call c.showcurr() to change the calendar to a certain year and month. The DOM is updated via setting <div id="divCal"></div>‘s innerHTML to a <table> element. The table element is built row by row in the this.showMonth method.

// Show current month
Cal.prototype.showcurr = function() {
    // sets dateHolder.innerHTML = stringified HTML
    this.showMonth(this.currYear, this.currMonth); 
};

The LWC version is simpler. Properties this.currYear and this.currMonth are replaced with the dateContext property. Instead of this.showMonth, we use this.refreshDateNodes() to creates list item nodes for the unordered list. Similar to the original JavaScript, the date nodes have different classes for whether they’re in the current month or not, if they’re selected, or if they represent today.

Here is the completed JavaScript file. The end component looks like this:

Lessons learned

  • Migration is not just copy and paste, it is an opportunity to improve the performance of our component. The LWC version renders fewer DOM nodes dynamically, since we moved nodes out from table header and changed the container element from table to unordered list.
  • Most of the conversion effort happens on the JS. Where can you use reactive properties instead of manual DOM manipulations? Which methods can you expose as public, for a parent component to call? Which methods make more sense as a component lifecycle method?
  • During the conversion, the new component might not work as expected. Here’s how to debug Lightning web components. The short version: you can set breakpoints in the browser, using Chrome Developer Tools → Sources

It is fun to create web components with the Lightning Web Components programming model! With live reload, straightforward debugging, and automated tests, the sky is the limit on what you can create.

Resources

About the author

Anny He works as a Developer Evangelist at Salesforce. She focuses on UI components and integrations with the Salesforce Platform. You can follow her on Twitter @annyhehe.

Leave your comments...

Convert a JavaScript Datepicker to a Lightning Web Component