7

I am building a calendar picker to accompany (and replace the default calendar date picker provided by some browsers) an input of type="date".

It is documented that for "simplicity" that Chrome's (and possibly other browsers) date input allows invalid dates to be inputted, but they will fail to validate (ie. 31/06/2017 can be inputted, but produces a "value" of "").

When an invalid date is entered into the input, how can I get the value of that invalid date?

var date_input = document.getElementById('date_input');
var output_div = document.getElementById('output_div');

output_div.innerHTML = date_input.value;

date_input.addEventListener('input', function(e){
    output_div.innerHTML = e.target.value;
});
<p>To quickly input an invalid date, select the day field and press the down arrow (on Chrome OSX this displays an empty div below the input instead of the invalid date). This will select 31/06/2017</p>
<input type="date" value="2017-06-01" id="date_input" />
<br />
<br />
<div id="output_div" />
TylerH
  • 20,799
  • 66
  • 75
  • 101
haxxxton
  • 6,422
  • 3
  • 27
  • 57
  • If you are using input type date, then how will the date value be invalid ? – sajalsuraj Jun 20 '17 at 05:27
  • @sajalsuraj, as mentioned, you _can_ enter (either manually, or by pressing on the arrow keys) a date such as `31/06/2017` (there are only 30 days in june). The input _allows_ this value to be entered and displayed, but does not return it as its "value" when queried using Javascript) – haxxxton Jun 20 '17 at 05:33

1 Answers1

5

Not like this

var date_input = document.getElementById( 'date_input' );
var output_div = document.getElementById( 'output_div' );

output_div.textContent = date_input.valueAsDate; // :-)

date_input.addEventListener( 'input', function( evt ){
    var val = date_input.value,
        sel = document.getSelection();
    if ( date_input.validity.badInput ) {
        // date_input.focus(); // with or without this,
        // date_input.select(); // this doesn't work but,
        // due to the added <span> wrapper:
        sel.selectAllChildren( date_input.parentElement );
        // this does. So:
        val = sel.toString() || function() {
            var copied = document.execCommand( 'copy' );
            sel.empty(); // clear the selection
            return "Invalid date" + ( copied ? " copied to clipboard." : "" );
        }();
    }
    output_div.textContent = val;
});
#output_div {
    margin-top: 1em;
}
<p>To quickly input an invalid date, select the day field and press the down arrow (on Chrome OSX this displays an empty div below the input instead of the invalid date). This will select 31/06/2017</p>
<span><input type="date" value="2017-06-01" id="date_input"></span>
<div id="output_div"></div>

Apparently since the value shown is actually the output of the shadow DOM child of the <input>, and we can't access the user-agent's shadow DOM, the visible value is effectively unknown to JS.

For accessibility reasons, the visible value has to be accessible to screen readers and the like, and can also be manually copied to the clipboard.

Having tried multiple ways of programmatically selecting and copying the text, I have run out of ideas and patience, and could only manage to copy it to the clipboard.

  • If someone else can figure out how to read the aria-value* attributes of the #shadow-root (user-agent) with JS, then you have a way forward.
  • If someone else can figure out how to assign the selection string to a var, you have another way forward.

However

Since the invalid input is limited to being predictably out of range, you could monitor what the last valid value was before it was changed to something invalid, then calculate what it must surely now be.

  • If using the datepicker, all the inputs will be valid.
  • If typing, monitor the keystrokes.
  • If using the arrow buttons, and the date was 30/06/2017 before a change that rendered it invalid, and 29/06/2017, 30/05/2017, 30/07/2017, 30/06/2016 and 30/06/2018 are all valid, the invalid date must be 31/06/2017.

I have not experimented (yet) with making that work, but am confident it can. I would be posting that solution with this answer, but half my day has been swallowed by this question already and I'm hungry. If a sincere interest is expressed, I will happily try and provide the codez.

Still no good

This will display invalid dates if they're created using the arrow buttons, and only if the last date was valid. I could probably find a way around that, but officially give up! Life's too short.

According to MDN:

... the displayed date format will be chosen based on the set locale of the user's operating system.

This would make tracking keystrokes unreasonably non-trivial.

However, I just discovered <input type="datetime-local"> which might solve that issue, and could possibly solve others.

So this does something useful:

But trying to force this to work is beginning to feel like trying to insert a watermelon where one would not usually be expected to fit.

It's a ham-fisted kludge with little to no charm, that is likely to break if anyone looks at it too hard. Best disclaimer ever right ;)

var date = "",
    input = document.querySelector( "input" ),
    output = document.querySelector( "output" ),
    bits = function( d ) {
        return [ d.getFullYear(), d.getMonth() + 1, d.getDate() ];
    },
    adjustedDate = function( o ) {
        var d = new Date( date ),
            b = bits( d ),
            year = b[ 0 ],
            month = b[ 1 ],
            day = b[ 2 ];
        switch ( o.i ) {
            case "d": return [ year, month, ( day + o.v ) || 31 ];
            case "m": return [ year, month + o.v, day ];
            case "y": return [ year + o.v, month, day ];
        }
    },
    calcDate = function() {
        var ad, vd, rd;
        [ { i: "d", v: 1 },
          { i: "d", v: -1 },
          { i: "m", v: 1 },
          { i: "m", v: -1 },
          { i: "y", v: 1 },
          { i: "y", v: -1 }
        ].forEach( ( v ) => {
            ad = adjustedDate( v ).join( "-" );
            vd = bits( new Date( ad ) ).join( "-" );
            if ( ad !== vd ) {
                rd = ad;
            }
        } );
        return rd.split( "-" ).map( ( v ) => {
            v = v.toString();
            return v.length === 1 ? "0" + v : v;
        } ).join( "-" ); // tired so mental
    },
    showDate = function() {
        output.textContent = date = ( input.value || calcDate() );
    };
showDate();
input.addEventListener( "input", showDate, false );
output {
    margin-left: 1em;
}
<p>To quickly input an invalid date, select the day field and press the down arrow. This will select 31/06/2017 and output the desired invalid date string.</p>
<form><input type="date" value="2017-06-01"><output></output></form>
Fred Gandt
  • 4,217
  • 2
  • 33
  • 41
  • Copying the text is an interesting workaround. I like it as a potential solution far better than the "however" section. Reason being that a date like `29/02/2012` has two places that, if changed, render the value invalid (up arrow on day, or up/down arrow on year), and i can think that the number keystokes option would only make the possibilities of invalid options hugely difficult to manage (especially given `min` and `max` attribute options) – haxxxton Jun 20 '17 at 12:10
  • 1
    @haxxxton - The practical alternative (if nothing else comes along) is to build your own datepicker. I ran into problems trying to use the HTML5 colorpicker and rather than wasting time trying to force it to behave, I just built my own - which is both satisfying and far sexier than the HTML5 version. I may work on the "however" solution anyway - for fun. If I have any success, I'll post it. – Fred Gandt Jun 20 '17 at 14:58
  • Yeah, if it wasnt for the handy convenience of what it does out of the box for the most part and time constraints on what im working on i would definitely go down that path. The calendar picker that ive built attached to it works great, its really only the interaction with the date picker itself that causing the issues at all.. keep us posted if you do find a solution to 'however' as id be happy to try and break it.. erm test it i mean ;) – haxxxton Jun 20 '17 at 23:16
  • Looks interesting, you're right it is relatively easy to break :P ie `29/02/2012` then up arrow on the year is resulting in a negative year.. and like you mentioned this only works for the first errorred date.. really appreciate the effort though! if only there was a way to detect the sections of the date that the keypress is occurring on :\ – haxxxton Jun 22 '17 at 07:28
  • Easy to break, but **possible** to fix. An alternative to the lack of access to the user-agent shadow-root, is to copy it and all its dependancies (manually) and [create a custom shadow](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow) from the copy and add it to a ``. That should give you access to the value(s) and in theory will function identically. – Fred Gandt Jun 22 '17 at 08:36