A Date Picker Control in Vanilla Javascript

A Date Picker Control in Vanilla Javascript

If you're putting together a commercial Javascript UI, you're probably going to use a framework like React or Angular (or Vue or Ember or... I can't keep track of all of the Javascript frameworks anymore). However, I often find myself relying on the availability of Javascript to put together quick-and-dirty prototypes or test pages that I don't really want the weight of a full framework on. Still, though, I do sometimes want some UI fluff like an interactive date picker. I put together a simple but functional, zero dependency, nonintrusive date picker control in plain Javascript that I've been using recently.

At a minimum, a date picker should show the current month and let the user page back and forth through the months (and optionally the years) to select a date. The days themselves ought to be presented as links so that the UI is intuitive as well. The basic functionality to do this is easy enough to accomplish in Javascript.

function showMonth(calendarDestId) {
  var target = document.getElementById(calendarDestId);

  var firstOfMonth = new Date();
  firstOfMonth.setDate(1);
  var lastOfMonth = new Date();
  lastOfMonth.setDate(1);
  lastOfMonth.setMonth(firstOfMonth.getMonth() + 1);
  lastOfMonth.setDate(lastOfMonth.getDate() - 1);
  var tbl = "<table>" +
    "<tr><th>Sun</th><th>Mon</th><th>Tue</th><th>" + 
    "Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr>" +
    "<tr>";
  var dow = firstOfMonth.getDay();
  for (var dow = 0; dow < firstOfMonth.getDay(); dow++) {
    tbl += "<td></td>";
  }
  for (var day = 1; day <= lastOfMonth.getDate(); day++) {
    if (dow == 7) {
      tbl += "</tr><tr>";
      dow = 0;
    }
    tbl += "<td><a href='#inv' onclick='return false;'>" + day + "</a></td>";
    dow++;
  }
  tbl += "</tr></table>";
  target.innerHTML = tbl;
}

Listing 1: show the current month

Example 1

Easy enough. I take advantage of Javascript's strong internal date support to find the end of the month by just rolling to the first of the next month and subtracting one from the day. The function accepts as input the ID of a div where the calendar itself should be inserted, as demonstrated in example 1. Right now, each day appears as a link, but clicking them doesn't cause anything to happen. I append href='#inv' to cause them to style correctly: I want the links to appear and behave as links, but always show as unclicked. Pointing them to an "invalid" reference accomplishes this. To make the links actually accomplish something, I can attach the date picker itself to an input control and allow the date selection to fill in the input box:

function selectDate(dateInputId, year, month, day)  {
  var dateInput = document.getElementById(dateInputId);

  if (dateInput)  {
    dateInput.value = year + "-" + 
      ((month < 10) ? "0" : "") + month + "-" + 
      ((day < 10) ?"0" : "") + day;
  }

  return false;
}

function showMonth(calendarDestId, dateInputId) {
  ...
  var year = firstOfMonth.getFullYear();
  var month = firstOfMonth.getMonth();
  ...
   tbl += "<td><a href='#inv' onclick='return selectDate(\"" + dateInputId + "\", " + year + ", " + 
	(month + 1) + ", " + day + ");'>" + day + "</a></td>";

Listing 2: record selected date in input control


Example 2

So far, so good, but the date picker is limited to the current month. At a minimum, I want to be able to scroll back and forth between the months and pick the one I'm interested in. I'll change my showMonth function to be self-referential and make the back and forth links reset the contents of the target div.

var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

function showMonth(calendarDestId, dateInputId, month, year) {
  var target = document.getElementById(calendarDestId);

  var month = month != null ? month : new Date().getMonth();
  var year = year || new Date().getFullYear();

  var firstOfMonth = new Date();
  firstOfMonth.setFullYear(year);
  firstOfMonth.setMonth(month);
  firstOfMonth.setDate(1);
  var lastOfMonth = new Date();
  lastOfMonth.setDate(1);
  lastOfMonth.setFullYear(year);
  lastOfMonth.setMonth(month + 1);
  lastOfMonth.setDate(lastOfMonth.getDate() - 1);
  var previousYear = year;
  var nextYear = year;
  var previousMonth = month - 1;
  var nextMonth = month + 1;
  if (previousMonth < 0) {
    previousYear -= 1;
    previousMonth = 11;
  }
  if (nextMonth > 11)  {
    nextYear += 1;
    nextMonth = 0;
  }
  var tbl = "<div class='calendarMonth'><a href='#inv' onclick='return showMonth(\"" + calendarDestId + 
    "\", \"" + dateInputId + "\", " + previousMonth + ", " + previousYear + ");'>&lt;&lt;</a>" +
    " " + monthNames[month] + " " + year + " " +
    "<a href='#inv' onclick='return showMonth(\"" + calendarDestId + "\", \"" + dateInputId + "\", " + 
    nextMonth + ", " + nextYear + ");'>&gt;&gt;</a></div>" : "") + 
    "<table>" +
    "<tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr>" +
    "<tr>";
  var dow = firstOfMonth.getDay();

Listing 3: Cycle through the months


Example 3

Could use some styling, but fully functional. However, date pickers aren't cool unless they pop up when the user selects the input field and disappear when the date is selected. This requires a tiny bit of CSS magic, but is pretty standard nowadays.

function showDatePicker(target) {
  var calendarDiv = document.createElement("div");
  calendarDiv.id = "calendar";
  calendarDiv.style.display = "block";
  calendarDiv.style.position = "absolute";
  leftOffset = 0;
  topOffset = 0;
  elem = target;
  while (elem != null)	{
    leftOffset += elem.offsetLeft;
    topOffset += elem.offsetTop;
    elem = elem.offsetParent;
  }
  calendarDiv.style.left = leftOffset;
  calendarDiv.style.top = topOffset + target.offsetHeight;
  calendarDiv.style.border = "1px solid";
  calendarDiv.style.background = "#fff";
  document.body.appendChild(calendarDiv);
  showMonth("calendar", target.id);
}

function removeCalendar() {
  var calendarDiv = document.getElementById("calendar");
  if (calendarDiv)  {
    calendarDiv.parentElement.removeChild(calendarDiv);
  }
}

function selectDate(dateInputId, year, month, day)  {
  var dateInput = document.getElementById(dateInputId);

  ...

  removeCalendar();

  return false;
}

<input type="text" id="dateInput" onfocus="showDatePicker(this)" />

Listing 4: Pop-up calendar

Date:

Example 4

I have to loop through the offsetParents and add them up to find the correct positioning of the calendar exactly underneath the input control; by setting the CSS position to absolute and inserting it at the end of the document, it appears to be a pop-up. Now I just add an onfocus handler to my date input and attach the newly created div to the document, it pops up under the date input as you'd expect. When the date is selected, it disappears.

Technically, since the calendar popup is positioned absolutely, it doesn't matter where in the document you append it. However, appending it to the very end of the document ensures that it doesn't change tab order of forms when it's inserted. This is actually the sort of thing that Shadow DOM and Web Components were designed to solve, but this works well enough for my purposes.

There's a bug here, though - if you click the date input twice, the calendar stops working. Why? Because the onfocus handler creates a second calendar div on top of the previous one. I need to check to see if the calendar is currently displayed before displaying a new one:

function showDatePicker(target) {
  if (document.getElementById("calendar"))  {
    return;
  }

  var calendarDiv = document.createElement("div");
  calendarDiv.id = "calendar";

Listing 5: Check for existing calendar

Date:

Example 5

While testing this page, I noticed that if you select the date picker in example 5 and return to example 4, the rest of the date pickers stop working. I can't (easily) fix this problem and still illustrate the reason for the change in listing 5, so if you did happen to do this, just refresh the page.

The other annoyance here is that the only way to dismiss the calendar pop-up is to select a new date. Standard UX for these sorts of things would dismiss them as soon as the user clicks anywhere else on the page. You can accomplish that by adding a click handler to the window as soon as the calendar itself is displayed: if the user's click is anywhere outside of the calendar, dismiss it.

function dismissCalendar(event)  {
  var cal = document.getElementById("calendar")

  if (cal)  {
    if (event.target == cal.targetId) {
      return;
    }

    elem = event.target;
    while (elem)  {
      if (elem.id == "calendar")  {
        return;
      }
      elem = elem.parentElement;
    }

    cal.parentElement.removeChild(cal);
    document.removeEventListener("mousedown", dismissCalendar);
  }
}

function showDatePicker(version, target) {
  if (document.getElementById("calendar"))  {
    return;
  }

  var calendarDiv = document.createElement("div");
  calendarDiv.id = "calendar";
  calendarDiv.style.display = "block";
  calendarDiv.style.position = "absolute";
  calendarDiv.style.border = "1px solid";
  calendarDiv.style.background = "#fff";
  calendarDiv.targetId = target;
  documnet.body.appendChild(calendarDiv);
  showMonth("calendar", target.id);

  document.addEventListener("mousedown", dismissCalendar);
}

function removeCalendar() {
  var calendarDiv = document.getElementById("calendar");
  if (calendarDiv)  {
    calendarDiv.parentElement.removeChild(calendarDiv);
  }

  document.removeEventListener("click", dismissCalendar);
}

Listing 6: Dismiss calendar popup on external click

Date:

Example 6

I use mousedown rather than click here, since click events aren't as consistent as mousedown. Since it's attached to the document body, every click invokes the handler, though, so I also have to check to see that the click isn't inside the date input (or else the handler would remove the calendar right away!) and also check to see if the click isn't a child of the calendar itself, or the calendar would disappear every time I tried to change the month.

I also have to create a custom HTML property on the calendar div itself to keep track of which target it's attached to, so that the event handler itself can tell which target element not to trigger calendar removal for. This allows me to support another common case, which is to allow multiple date inputs. I only want one calendar to appear at a time, but I want it to move from one date element to the next as the user selects them. The only minor change I have to make to support this case is to check not just whether the calendar is currently showing, but if the calendar is currently showing for the currently selected date input field. The only reason this is actually needed is to support the case where the user is tabbing through the input fields (do users still do this?), since there's no top-level onclick that's captured to hide the currently displayed date picker.

function showDatePicker(version, target) {
  var currentCalendar = document.getElementById("calendar");
  if (currentCalendar)  {
    if (currentCalendar.targetId == target) {
      return;
    } else  {
      removeCalendar();
    }
  }

Listing 7: Show the calendar if the user tabs from one control to the next

Start Date: End Date:

Example 7: Multiple date pickers

Finally, this could stand a bit of usability polish: every time you open the date picker, it defaults to the current month, rather than the date selected. There's also no styling here - at a minimum, I want to style the month selector to be centered so that the back-and-forth links don't jump around as I scroll through the months.

function showDatePicker(version, target) {
  var currentCalendar = document.getElementById("calendar");
  if (currentCalendar)  {
    if (currentCalendar.targetId && currentCalendar.targetId == target) {
      return;
    } else  {
      removeCalendar();
    }
  }

  var selectedMonth = null;
  var selectedYear = null;

  var dateSelectedStr = target.value;
  if (dateSelectedStr) {
    var dateSelected = new Date();
    if (dateSelectedStr != "")  {
      var components = dateSelectedStr.split("-");
      dateSelected.setFullYear(components[0]);
      dateSelected.setMonth(components[1] - 1);
      dateSelected.setDate(components[2]);
    }
    selectedMonth = dateSelected.getMonth();
    selectedYear = dateSelected.getFullYear();
  }
...
showMonth(version, "calendar", target.id, selectedMonth, selectedYear);

Listing 8: default to the selected month

Date:

Example 8: with a bit of styling

Since I'm parsing the input on focus, I also want to prohibit typing with an onkeydown="return false;" guard.

So there you have it: a minimal but functional, zero-dependency date picker in about a hundred lines of Javascript.

Add a comment:

Completely off-topic or spam comments will be removed at the discretion of the moderator.

You may preserve formatting (e.g. a code sample) by indenting with four spaces preceding the formatted line(s)

Name: Name is required
Email (will not be displayed publicly):
Comment:
Comment is required
loon, 2023-08-16
Could you say something about the security implications (if any) of this? My gut tells me that not having a huge framework in the background lowers the possible points of failure, but on the other hand, my gut never saw a book on CS :)
Cheers for this blog (sad it ends in '21)
My Book

I'm the author of the book "Implementing SSL/TLS Using Cryptography and PKI". Like the title says, this is a from-the-ground-up examination of the SSL protocol that provides security, integrity and privacy to most application-level internet protocols, most notably HTTP. I include the source code to a complete working SSL implementation, including the most popular cryptographic algorithms (DES, 3DES, RC4, AES, RSA, DSA, Diffie-Hellman, HMAC, MD5, SHA-1, SHA-256, and ECC), and show how they all fit together to provide transport-layer security.

My Picture

Joshua Davies

Past Posts