Svelte datepicker in TypeScript using native browser support

In every frontend/SPA project – at some point – we need a datepicker. Usually we pick a framework-compatible npm package like svelte-datepicker, ag-datepicker, and so on. In my latest Svelte project I used mymth/vanillajs-datepicker that works pretty weel, a lot of options, flexible and you can customize the look via custom css.

A downside of this, is that in my experience we need to try to avoid as much as possibile external dependencies, something you learn when you maintain a project for a long time, and things starts to be unsupported. At that point you have a big problem. It’s more a business decision, than a technical decision.

So what if it is possibile to have a datepicker without external dependencies? I didn’t know, but now almost all browser support <input type="date />!
You can find all the technical details in the MDN docs.

Here an example in Svelte and TypeScript, using a custom action. Add this in a file native-date-picker.ts.
You can find a working Svelte REPL here (Javascript).
PS1: note the ActionReturn that allows Visual Studio Code (or any Svelte tooling) to know which events or attributes the action enriches the DOM element.
PS2: I added an example about how to open “manually” the datepicker, but the native datepicker has a button integrated, so it should not be needed, but I found interesting the showPicker() method.

A limitation of the native datepicker is that you cannot force to show to the user a specific date format, but the browser language localization is used.
I’m not aware of any trick to bypass this – pretty annoying – limitation.

import type { ActionReturn } from 'svelte/action';

export type DatepickerOptions = {
	maxDate?: Date;
	minDate?: Date;
}

export type DatePickerContext = {
	date?: Date,
	options?: DatepickerOptions
}

interface Attributes {
	'on:changeDate': (e: DatePickerChangeEvent) => void
}

export type DatePickerChangeEventPayload = { date: Date | undefined };
export type DatePickerChangeEvent = CustomEvent<DatePickerChangeEventPayload>;

/** ISO8601 as explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date */
const asNativeDateString = (date: Date) => {
	const month = String(date.getMonth() + 1).padStart(2, '0');
	const day = String(date.getDate()).padStart(2, '0');
	return `${date.getFullYear()}-${month}-${day}`;
};

/**
 * Action for native DatePicker, see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date.
 *
 * You can manually open the picker via `<HTMLInputElement>.showPicker();`
 * @event changeDate
 * @example
 * <script lang="ts">
 * 	let ctrl: HTMLInputElement;
 *
 * function onChangeDate(ev: DatePickerChangeEvent) {
 *   const date = ev.detail.date;
 *   console.log(date);
 * }
 *
 *  function openDatePicker() {
 *   ctrl.showPicker();
 *  }
 * </script>
 *
 * <input type="date" use:nativeDatePicker="{{ date: null, options: { minDate: new Date(1970, 0, 1) } }}" bind:this="{ctrl}" on:changeDate="{onChangeDate}" />
 * <button type="button" on:click="{openDatePicker}"></button>
 */
export function nativeDatePicker(node: HTMLElement, data: DatePickerContext): ActionReturn<DatePickerContext, Attributes> {
	const input = node as HTMLInputElement;

	if (!(node instanceof HTMLInputElement) && input.type !== 'date') {
		throw new Error('Unsupported type');
	}

	function init(data: DatePickerContext) {
		input.value = data.date ? asNativeDateString(data.date) : '';
		input.max = data.options?.maxDate ? asNativeDateString(data.options.maxDate) : '';
		input.min = data.options?.minDate ? asNativeDateString(data.options.minDate) : '';
	}

	function handleChange(_: Event) {
		const date = input.valueAsDate ?? undefined;
		node.dispatchEvent(new CustomEvent<DatePickerChangeEventPayload>('changeDate', { detail: { date: date } }));
	}

	init(data);

	node.addEventListener('change', handleChange);

	return {
		update(newData: DatePickerContext) {
			init(newData);
		},

		destroy() {
			node.removeEventListener('change', handleChange);
		},
	};
}

And then a Svelte component using it:

<script lang="ts">
let ctrl: HTMLInputElement;

function onChangeDate(ev: DatePickerChangeEvent) {
  const date = ev.detail.date;
  console.log(date);
}

function openDatePicker() {
  ctrl.showPicker();
}
</script>

<input type="date" use:nativeDatePicker="{{ date: null, options: { minDate: new Date(1970, 0, 1) } }}" bind:this="{ctrl}" on:changeDate="{onChangeDate}" />
<button type="button" on:click="{openDatePicker}"></button>

Add a Comment

Your email address will not be published. Required fields are marked *