Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

A Button Only Date Picker and JavaScript Date Control Binding


:P
On this page:

Ah, date pickers! My favorite HTML UI feature to harp on, because it's such a shit show. How many times do we have to re-create a DatePicker for various different tools or technologies? Too many to keep track of for me...

However, it turns out that desktop browser vendors have finally gotten the memo and made at least some minimal effort in cleaning up the lackluster date controls in Chromium, WebKit and Firefox to the point that one can actually consider using them for date input universally now. They still only work for some feature subsets but if you just need single date entry they are actually reasonably usable now.

But even today - 50 million years after the creation of the HTML5 standard which first introduced <input type="date" /> we still have this bare bones type control:

  • Supports only UTC time, thus forcing just about any apps to convert to and from local time (with no native JS support for that)
  • No control over the <input> control UI
  • No easy way to use the calendar without the text box

In short, the control is designed for exactly one simple use case, which is date entry in a specific format with a specific non-customizable UI. For everything else there's still a requirement of using some sort of third party control.

This post is about using what's there - warts and all - and making the process of using date pickers a little easier by handling the date formatting to and from and by providing a button only calendar popup.

I'll talk about the following:

  • A Date-Input wrapper component
  • Button-only Calendar Popup
  • A datepicker-button Vue component

You can find the code for these components on GitHub and a slightly simplified sample on CodePen:

Date Controls and Dates

Why this post and small library? In short, everytime I decide to just use the native date control I have to remember how the control works and re-figure the logic and conversions required to make binding and unbinding work with a typical application that expects to display and enter dates in local time format.

If you're using plain JS code, there's a bit of work you have to do coerce dates into the right format both when binding and then again when retrieving the updated value from the control. So to do this once and for all I created a small, plain JS wrapper that makes this easier by just assigning the date value to a control and allowing for a callback when the date is updated.

How the native Date Control works

The way the date input controls natively works is a pain, as dates have to be formatted as a string in ISO8601 format without a time component:

<input type="date" 
       value="2022-11-29" 
       max="2022-12-05"/>

There are two problems with this:

  • value has to be assigned as a string (not a date)
  • The value format is a universal format (yyyy-MM-dd) which typically is different than local time format
  • The value has to be provided in UTC time not the local time

Note that there's a type="datetime-local" variation that doesn't need local date adjustment, but that displays the date and time picker, rather than just the date picker. There's not date-local (Duh!). And before you say "But Rick, a date doesn't have a time component", realize that dates always have a time associated with them, even if it's 00:00 and even that date can end up changing the date by +1 or -1 day depending on the timezone that you are in! It's not as easy as lopping off the time value and setting it to 00:00.

So type="date" controls have to be adjusted for UTC time. If you've never worked with the datepicker before you may not notice this subtle difference until you run into atime value on the date that ends up rolling over into the next (or previous) day. 😢

Local Date Fix ups

So even for a 'date' only value, in order to reliably bind to the date control, the date has to be adjusted to UTC time. Then, when the control value has been updated, the conversion process has to be reversed to get back to a local time value from the UTC date that gets stored in the control's value property.

The simple DatePickerNative component automatically fixes up the date both on binding and unbinding of values.

Date UTC Conversions

The core UTC time calculations needed are from a local Date value to a UTC string value, and from a UTC string value back to a local Date value:

// Local Date to UTC string Date
function localToGmtDate(localDate) {
    return localDate && new Date(localDate.getTime()-(localDate.getTimezoneOffset()*60*1000)).toISOString().split('T')[0];        
}

// UTC String Date to Local Date
function utcToLocalDate(utcDate) {
    return new Date(utcDate.getTime()+(utcDate.getTimezoneOffset()*60*1000));        
}

The first function returns a string value that can be bound to the value property. The second function returns a new Date object from a string value, which contains the local date and a 00:00 time.

Without going into too much detail of the component, here is how it uses these function to bind the value and the pick it up the result when the date value changes via the change event:

// called from initialization
function datePickerBind(element, dt) {        
    var newDate = localToGmtDate(dt);
    
    opt.element.dateValue = dt;              // pseudo property on element to hold Date value
    opt.element.value = newDate;             // the  string formatted UTC date 

    if (opt.min)
        opt.element.min = normalizeMin(opt.min);        
    if (opt.max)
        opt.element.max = normalizeMax(opt.max);
}

// called from control's `change` event
function datePickerUnbind(event) {        
    var dt = event.target.valueAsDate;
    let newDate =  utcToLocalDate(dt);
    
    opt.element.dateValue = newDate;      // pseudo property on element
    opt.activeDate = newDate;             // instance property 

    // user provided callback
    if(opt.onDateChanged){  
        opt.onDateChanged(newDate, event, _this);
    }
}

Wrapping into a simple Component

To make the process of assignment and retrieval easier I use a component that basically maps a date value to an element and then allows specifying a callback handler when the date changes. You also can set min/max values to limit the displayed date range of the picker.

In use this looks something like this:

<!--
*** USM globals use these imports instead:
<script src="datepicker-native.js"></script>
<script src="date-formatter.js"></script>
-->

<script type="module">
    import  DatePickerNative from "./datepicker-native.esm.js";
    
    var startDate = new Date(2023,1,7);
    var el = document.getElementById("DatePickerInput");
     
    // option configuration syntax        
    let dpn = new  DatePickerNative({            
        element: el,
        activeDate: startDate,
        // + or - 5 days
        min: 5,
        max: 10,
        // passed Date and Event object
        onDateChanged: function(dt, event, instance) {                
            console.log( dt.toLocaleString(), " onDateChanged() result date value")   

            // element gets a dateValue property
            var el = document.getElementById("DatePickerInput");
            console.log(el.dateValue.toLocaleString()," control pseudo property `dateValue`");

            console.log(dpn.options.activeDate.toLocaleString," instance.option.activeDate");
        }        
    });
</script>    

If you're using a standard date picker control like this:

<input id="DatePickerInput" type="date" class="form-control" />

here's what you're going to see in a Chromium style browser (Edge here):

Plain JavaScript

Because this component uses plain, old JavaScript it'll work in any JavaScript based environment. You can drop into a standalone page, or use it with a framework.

This component is simple, small and self contaiend and does little more than collecting the input parameters, assigning the date value on initialization, and then hooks up an event to detect the date change, that is then forwarded to code you provide.

If you want to use it, or are curious about the code, you can grab or check out the complete code on GitHub:

Button Only Lookups

The genesis of this example was my need to add a date picker to an application that is based on a button, rather than on a full <input type='date' /> control.

What I want to do is essentially the following:

Where the button is independent of the date display, or there is no date display at all.

To make this work I can use the save DatePickerNative control described above, and apply some custom HTML and styling to essentially hide the input control part of the stock date picker control.

How the native Button only Component works

To create a button only calendar involves a few steps:

  • Add the button HTML
  • Add DatePickerNative.js library
  • Add DatePickerNative.css styles (or you can inline the CSS)

Let's start with the HTML:

<button id="DatePicker" class="datepicker-native btn btn-secondary btn-sm">
    <i class="far fa-calendar-alt"></i>
    <input type="date" class="datepicker-native-input" />
</button>

I'm using font-awesome for the icon here, but you can use whatever you like for the content for the button - text, image, icon, it doesn't matter. The btn styles are bootstrap and likewise, you can use whatever you want or no styling for the button.

The important and only required pieces are the two styles:

  • datepicker-native on the<button>
  • datepicker-native-input on the embedded <input type="date" /> control

The trick to making the button work without the input control visible, is to effectively make it invisible but still active and then overlaying the button content over it.

The key to active but invisible behavior is the CSS that uses z stack positioning:

.datepicker-native {
    position: relative;
}
.datepicker-native-input {
    position: absolute;
    overflow: hidden;
    width: 100%;
    height: 100%;
    right: 0;
    top: 0;
    opacity: 0;
}
.datepicker-native-input::-webkit-calendar-picker-indicator {
    position: absolute;
    right: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
}

The button is marked as position:relative with the input control made full width and height inside of the button. The opacity is 0 so the input control is not visible, but because it's position: absolute it sits ontop of the button content and is still responsive. So now when you click the button you really are clicking on the invisible date control which fires the calendar.

There's one more important piece to this: The .datepicker-native-input::-webkit-calendar-picker-indicator style makes the entire input date control clickable instead of just the calendar button of the native control. Interestingly FireFox makes the entire control clickable by default, so if you click anywhere on the control it always opens the calendar.

The script operation then activates the initial date binding which adjusts the date appropriately and allows any changes to fire a callback:

var startDate = new Date(2022,10,10);
var el = document.getElementById("DatePicker");
 
// *** Create the component and handle the callback
DatePickerNative(el, startDate, function(dt, event) {
    // updated date is returned
    console.log(dt.toLocaleString());

    // update date display   
    showDate(dt,"ActiveDate");
});

showDate(startDate,"ActiveDate"); // initial display 

Vue Component

For good measure I ran into all of this originally within a Vue application and I initially build a Vue component for a drop-in data-button component only later back-fitting the plain JS component.

The Vue version is a bit less generic as it uses bootstrap and font-awesome in the template, but you can customize the template as you see fit.

The component can directly bind dates and min/max values (activeDate is a date value on the Vue model):

<date-picker-button
    v-bind:date-value="activeDate"
    v-on:update:dateValue="dateUpdated($event)"
    v-bind:min="7"
    v-bind:max="7"
></date-picker-button>

Date value binding is one way only, and instead you have to handle an event to capture and update the new date value - dateUpdated() in the code below:

export default {
    name: "ServiceOrderListView",
    components: { DatePickerButton },
    data() {
        vm = this; // hang on to proxy reference
        return {
            // this is our date binding value
            activeDate: mdApp.global.lastAssignedServiceSearchDate,
        };
    },
    methods: {
        // Date Update Handler 
        dateUpdated(newDate){
            if (vm.activeDate === newDate) return;
            
            // this updates the model
            vm.activeDate = newDate;
            
            // now re-run the filtered list with the new date
            vm.getServiceOrders();
        }
    }
}    

Note: The event does not fire if you select the currently selected date - the value has to actually change for the event to fire.

Vue Component Implementation

I ran into the button-only requirement originally within a Vue application, so I initially built a Vue component for a drop-in date-button component. Only later did I figure this would be more useful as a generic JavaScript component for other applications or single pages that need to capture and display dates.

The Vue version is a bit less generic as it uses bootstrap and font-awesome in the template, but you might find this useful anyway as you can customize the layout for your own application.

Here's the Vue Component:

<template>
    <button v-bind:class="buttonClass  + ' ' + addButtonClass" v-bind:title="title">
        <i v-bind:class="buttonIconClass"></i>

        <input type="date" class="datepicker-button-input"
               :value="datePickerBind(dateValue)"
               @input="datePickerUnbind($event.target.valueAsDate,dateValue)"
        />
    </button>
</template>

<script>
export default {
    name:"DatePickerButton",

    props: {
        title: {type: String, default: "Select a date"},
        dateValue: {type: Date, default: new Date() },
        dateId: { type: String, default: new Date().getTime().toString() },

        buttonClass: {type: String, default: "btn btn-secondary btn-sm datepicker-button" },
        addButtonClass: {type: String, default: "" },
        buttonStyle: {type: String, default: "" },
        buttonIconClass: { type: String, default: "fad fa-calendar-alt fa-fw" }
    },
    methods: {
        datePickerBind(dt) {
            return dt && new Date(dt.getTime()-(dt.getTimezoneOffset()*60*1000)).toISOString().split('T')[0];
        },

        datePickerUnbind(dt) {
            let newDate =  new Date(dt.getTime()+(dt.getTimezoneOffset()*60*1000));
            this.$emit("update:dateValue",newDate, this.dateId);
        }
    },
}
</script>

<style scoped>
.datepicker-button {
    position: relative;
}
.datepicker-button-input {
    position: absolute;
    overflow: hidden;
    width: 100%;
    height: 100%;
    right: 0;
    top: 0;
    opacity: 0;
}
.datepicker-button-input::-webkit-calendar-picker-indicator {
    position: absolute;
    right: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    opacity: 0;
    cursor: pointer;
}
</style>

The date-picker-button.vue component is tiny and you can simply copy and drop it into your project as needed.

Summary

Date handling in JavaScript is a pain since there's limited support for date conversions and the native controls are extremely rudimentary in features and customization options. But with a little bit work they can be made to work reasonably well, and I hope the code here makes that process a little bit easier, or at minimum helps you with doing your own data adjustments for your custom date handling...

Time's up!

Resources

this post created and published with the Markdown Monster Editor
Posted in HTML  JavaScript  

The Voices of Reason


 

Philipp Smirnov
February 06, 2023

# re: A Button Only Date Picker and JavaScript Date Control Binding

How accessible is it? From my experience date pickers have terrible history with accessibility - some have problems with keyboard control and many have problems with screen readers.


Stuart
February 07, 2023

# re: A Button Only Date Picker and JavaScript Date Control Binding

Nice work, Rick, and absolutely agree with you that the native Date solutions are a PITA, to put it mildly!

But, I would (with the greatest of respect) take issue with your native Button only component. It's not valid HTML to include an interactive component within a button (see https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) and it's obviously not going to be super accessible either... Although it would require some refactoring for your JS, I think it would be better if you wrapped the two items (button and input) inside a div or a span, then you can keep the component valid and accessible (well, depending on how you'd choose to do it, of course! 😄).

Saying that, you make a number of great points, and as I say, you have certainly struck a chord with me and no doubt thousands of others!!


Rick Strahl
February 07, 2023

# re: A Button Only Date Picker and JavaScript Date Control Binding

@Stuart those are good points...

However, I think the accessibility issue is overstated here, because the popup is actually a separate UI component from the input control that works separately, and as far as I know the calendar is then still accessible (as far as the browser UI is accessible that is). IOW, the popup is no different than if you click the button in the date input - the difference is you press a separate button and that button should work with a screen reader.

I appreciate the feedback and to be clear I'm making a guess here, so I could be off base. It'd be great if someone with an actual screen reader can verify that this doesn't actually work. I suspect it does...

As to the invalid HTML - fair enough but again, but it works and it's the only way you can hide the input control. This type of thing is used for many solutions that have to get around the fucked up default UI that browsers provide (file upload buttons being another one). The whole point of that is to hide the control because there's no other way to do that so if it takes a hack that is invalid HTML so be it 😄


Rick Strahl
February 07, 2023

# re: A Button Only Date Picker and JavaScript Date Control Binding

I've added a CodePen link to make it easier to play around with this code:


Richard
February 09, 2023

# re: A Button Only Date Picker and JavaScript Date Control Binding

As an alternative to making an invisible input overlay the button, all major browsers now support the HTMLInputElement.showPicker method.

https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/showPicker

The only down-side is that it won't work in a cross-origin <iframe> for anything other than file and colour pickers.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024