Skip to content

Dates in a form

LO-TEK has no date picker. The browser has one, and it is better than ours was.

<lt-field>
<label class="lt-label" for="deliver-by">Deliver by</label>
<input class="lt-input" data-variant="date" id="deliver-by"
name="deliver-by" type="date"
min="2079-01-01" max="2079-12-31">
<p class="lt-help-text">Type the date as 31/05/2079, or pick it in the calendar.</p>
</lt-field>

If the code that renders the markup can call a builder function, in TypeScript, Kotlin or Java, it writes the attributes itself and does not need <lt-field>:

import { lt } from "@lo-tek/interfaces"
const field = lt.field({ id: "deliver-by", help: true, invalid })
const control = lt.input({ type: "date", state: field.state })

We had one, <lt-calendar>, and removed it. The sums did not add up:

TaskWho does it
Opening the calendar panelthe browser
Keyboard navigation in the panelthe browser
Translation and first day of the weekthe browser, following the user’s own settings
Valid rangemin and max
ISO value to the serverthe browser
Wiring the label, help text and error textlt.field()

What was left for a separate component was nothing, apart from the panel looking the same in every browser. That is the same trade-off we have already made for <select>: the dropdown is drawn by the operating system, and nobody misses it looking the same in every browser.

The panel is also transient. It is not part of the page; it opens and closes again.

Three browser differences you need to know

Section titled “Three browser differences you need to know”

The first two are tested in Chromium, Firefox and WebKit, not read in a specification, and the third was seen in Safari on an iPhone. The tests used a Norwegian locale (nb), where the browser shows the date as 31.05.2079, so the test results below use that format. The help text uses the page’s own format, 31/05/2079.

WebKit requires the separators. If the user types 31052079 without full stops, the field stays empty with validity.badInput, and the form sends nothing. 31.05.2079 works. So the help text should say how to write the date, with an example in the page’s format, such as “Type the date as 31/05/2079, or pick it in the calendar.”

Chromium accepts the wrong order as valid. 20260531 became the year 60531, and validity.valid was still true. So min and max should always be set. Without them a typo gets through as a silently wrong date.

Safari on an iPhone lets the user pick a date outside min and max. The picker offers every year, and 25 April 2081 lands in a field limited to 2079. The field gets rangeOverflow, so the error message must say which dates are allowed, not that the date does not exist, and the server must check the range too.

const date = formData.get("deliver-by")
if (typeof date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return error("Enter the date as day, month and year.")
}
// 2079-02-31 has the right shape, and is no date.
const [year, month, day] = date.split("-").map(Number)
const parsed = new Date(Date.UTC(year, month - 1, day))
if (parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
return error("Enter a date that exists.")
}
if (date < "2079-01-01" || date > "2079-12-31") {
return error("The board only takes jobs for 2079.")
}

The field always sends ISO when the browser accepted the date, and ISO dates compare correctly as strings. But badInput means the server gets an empty string from someone who meant to type something, and your app has to point out that difference.

  • We do not validate the date. min and max are the browser’s check, and it can be bypassed. The server has to do it again.
  • We do not translate the error messages. The browser’s own bubbles (“Please enter a valid value”) come in the browser’s language. If you want them in the page’s language, you have to turn them off with novalidate and show <p class="lt-error-text"> yourself.
  • We do not handle date ranges. From and to are two fields, and that one has to come before the other is your app’s rule.
  • We have no date picker you can style. If you need a calendar that looks the same in every browser, that is a separate project in your app, not something that lives here.