I'm developing a custom date input format to enhance user experience in scenarios where birthdays and expiration dates are inputted. Traditional date fields and date picker libraries often fall short in offering intuitive user interactions. My script has been designed to:
- Enforce a month value range from 01 to 12.
- Ensure day values stay within the 01 to 31 range.
- Set the year value boundaries between 1900 and 2100.
- If two digits year starting with 0 or 3 is entered year is assumed 1900s.
- If two digits year starting with 4 - 9 is entered year is assumed to be 2000s.
- Automatically advance from the month to day section when the first digit of the month exceeds 1.
- Move the cursor to the year section when the initial digit of the day goes beyond 3.
- Recognize a manually entered slash (/), and, if needed, precede it with a leading zero before transitioning to the next segment.
- Filter out non-numeric inputs, with the sole exception being a manually entered slash (/).
- Adjust a 00 entry for month or day to 01 for better consistency.
- Permit the backspace functionality to clear fields without default autofill interruptions.
Added:
- Form validation using pattern attribute, thanks morbusg
- Allowed for pasting ISO yyyy-mm-dd formats, along with mm/dd/yyyy and m/d/yyyy, thanks J-H
My goal is to optimize user interactions, ensuring the underlying code is robust, reliable, and widely compatible across browsers.
const dateInputs = document.querySelectorAll('.date-input');
dateInputs.forEach(dateInput => {
let lastValue = ""; // To keep track of the previous value for backspace detection
dateInput.addEventListener('input', function() {
let val = this.value;
// If backspacing, skip the rest of the formatting
if (lastValue && lastValue.length > val.length) {
lastValue = val;
return;
}
if (val.endsWith('/')) {
val = val.slice(0, -1); // Remove the manually entered slash
if (val.length === 1) {
val = '0' + val + '/';
} else if (val.length === 4) {
val = val.slice(0, 3) + '0' + val.slice(3) + '/';
}
}
val = val.replace(/\D/g, ''); // Remove any non-digit characters
// Apply the MM/DD/YYYY format with constraints
if (val.length === 1 && parseInt(val, 10) > 1) {
val = '0' + val + '/';
} else if (val.length >= 2) {
let month = parseInt(val.substring(0, 2), 10);
if (month === 0) month = 1;
if (month > 12) month = 12;
val = (month < 10 ? '0' + month : month) + '/' + val.substring(2);
}
if (val.length === 4 && parseInt(val.substring(3, 4), 10) > 3) {
val = val.substring(0, 3) + '0' + val.substring(3) + '/';
} else if (val.length >= 5) {
let day = parseInt(val.substring(3, 5), 10);
if (day === 0) day = 1;
if (day > 31) day = 31;
val = val.substring(0, 3) + (day < 10 ? '0' + day : day) + '/' + val.substring(5);
}
if (val.length === 7) {
const yearStart = parseInt(val.substring(6, 7), 10);
if (yearStart >= 4 && yearStart <= 9) { // If between 4 - 9 assume 1900's
val = val.substring(0, 6) + '19' + val.substring(6);
} else if (yearStart === 0 || yearStart === 3) { // If between 0 or 3 assume 2000's
val = val.substring(0, 6) + '20' + val.substring(6);
}
} else if (val.length >= 10) {
let year = parseInt(val.substring(6, 10), 10);
if (year < 1900) {
year = 1900;
} else if (year > 2100) {
year = 2100;
}
val = val.substring(0, 6) + year;
}
this.value = val;
lastValue = val;
dateInput.addEventListener('paste', function(e) {
e.preventDefault();
const clipboardData = e.clipboardData || window.clipboardData;
let pastedData = clipboardData.getData('Text');
if (isISOFormat(pastedData)) {
this.value = convertFromISOFormat(pastedData);
}
else if (isShortDateFormat(pastedData)) {
this.value = convertFromShortFormat(pastedData);
}
else if (isStandardDateFormat(pastedData)) {
this.value = pastedData;
}
else {
// If it doesn't match any of the allowed formats, don't set the value.
this.value = "";
alert("Please paste a date in the correct format!");
return;
}
let event = new Event('input', {
'bubbles': true,
'cancelable': true
});
dateInput.dispatchEvent(event); // manually dispatch input event to trigger your input handler after paste
});
});
// Prevent users from entering non-digit characters, except slash
dateInput.addEventListener('keydown', function(e) {
if (!isAllowedDateKeyEvent(e)) {
e.preventDefault();
}
});
});
// Check if it's in yyyy-mm-dd format
function isISOFormat(date) {
const isoPattern = /^\d{4}-\d{2}-\d{2}$/;
return isoPattern.test(date);
}
// Convert yyyy-mm-dd to MM/DD/YYYY
function convertFromISOFormat(isoDate) {
const parts = isoDate.split('-');
return `${parts[1]}/${parts[2]}/${parts[0]}`;
}
// Check if it's in m/d/yyyy format
function isShortDateFormat(date) {
const shortDatePattern = /^\d{1,2}\/\d{1,2}\/\d{4}$/;
return shortDatePattern.test(date);
}
// Convert m/d/yyyy to MM/DD/YYYY
function convertFromShortFormat(shortDate) {
const parts = shortDate.split('/');
const month = parts[0].padStart(2, '0');
const day = parts[1].padStart(2, '0');
const year = parts[2];
return `${month}/${day}/${year}`;
}
// Check if it's in mm/dd/yyyy format
function isStandardDateFormat(date) {
const standardDatePattern = /^\d{2}\/\d{2}\/\d{4}$/;
return standardDatePattern.test(date);
}
function isAllowedDateKeyEvent(e) {
let charCode = e.keyCode;
// Allow CTRL key or CMD key on Mac (e.metaKey)
if (e.ctrlKey || e.metaKey) {
return true;
}
if (charCode > 31 && (charCode < 48 || charCode > 57) && charCode !== 37 && charCode !== 39 && charCode !== 8 && charCode !== 46 && charCode !== 191) {
return false;
}
return true;
}
<input type="text" class="date-input" placeholder="mm/dd/yyyy" pattern="\d{2}/\d{2}/\d{4}" inputmode="numeric">
1 Answer 1
Unsafe input modification
There are way too many issues with this function for it to be safe to use in the wild.
The main reason is when a client modifies the data (change an entered date). Your function makes changes to the input away from the caret, this should never happen and may not be noticed by the client, especially if "tab" or "enter" moves focus to the next input.
There are also many more date formats than the US scheme "mm/dd/yyyy"
Most common is "dd/mm/yyyy"
Which is incompatible with your function. Don't expect clients to actually read the placeholder (which is gone on the first entered character).
Locale dates
Let the browser handle the date format, just use the input type "date"
Example
<input type="date" min="1900年01月01日" max="2100年01月01日"/>
dateInput.addEventListener("input", (e) => {
console.clear();
console.log("Input string.: " + dateInput.value); // Show value as stored in imput element
const inputDate = new Date(dateInput.value); // Create the correct date defined by clients locale setup
console.log("Date object..: " + inputDate);
console.log("Date value...: " + inputDate.valueOf());
console.log("Date JSON....: " + inputDate.toJSON());
console.log("US format....: " + inputDate.toLocaleString("en-US"));
console.log("Common.......: " + inputDate.toLocaleString("en-AU"));
});
<input type="date" id="dateInput" min="1900-01-01" max="2100-01-01"/>
-
\$\begingroup\$ Yeah but sometimes a date picker isn't optimal and a user wants to fill in a date. There in lies the issue. \$\endgroup\$tony– tony2023年11月17日 02:33:32 +00:00Commented Nov 17, 2023 at 2:33
3
" impressed me as trouble, since a pre-determined (paste) string of valid user input characters would be treated differently based on such details. I had in mind unconditionally accepting 1 input char, then 1 msec later "fix it up". \$\endgroup\$