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;
}
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>";
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 + ");'><<</a>" +
" " + monthNames[month] + " " + year + " " +
"<a href='#inv' onclick='return showMonth(\"" + calendarDestId + "\", \"" + dateInputId + "\", " +
nextMonth + ", " + nextYear + ");'>>></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();
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)" />
Date:
I have to loop through the offsetParent
s 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";
Date: 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);
}
Date:
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();
}
}
Start Date:
End Date: 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);
Date:
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:
Cheers for this blog (sad it ends in '21)