Datepicker
Calendar interface for selecting and formatting dates.
TailwindCSS
Alpine.js
package showcase
import "github.com/axzilla/templui/pkg/components"
templ DatepickerDefault() {
<div class="w-full max-w-sm">
@components.Datepicker(components.DatepickerProps{})
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just Datepicker
@components.DatepickerScript()
2. Use the component:
@components.Datepicker(components.DatepickerProps{...})
Examples
With Label
package showcase
import "github.com/axzilla/templui/pkg/components"
templ DatepickerWithLabel() {
<div class="w-full max-w-sm space-y-2">
@components.Label(components.LabelProps{
Text: "Pick a date",
For: "datepicker-with-label",
})
@components.Datepicker(components.DatepickerProps{
ID: "datepicker-with-label",
})
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
Custom Placeholder
package showcase
import "github.com/axzilla/templui/pkg/components"
templ DatepickerCustomPlaceholder() {
<div class="w-full max-w-sm">
@components.Datepicker(components.DatepickerProps{
Placeholder: "When is your birthday?",
})
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
Selected Date
package showcase
import (
"github.com/axzilla/templui/pkg/components"
"time"
)
templ DatepickerSelectedDate() {
<div class="w-full max-w-sm">
@components.Datepicker(components.DatepickerProps{
Value: time.Now(),
})
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
Disabled
package showcase
import "github.com/axzilla/templui/pkg/components"
templ DatepickerDisabled() {
<div class="w-full max-w-sm">
@components.Datepicker(components.DatepickerProps{
Disabled: true,
})
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
Formats
package showcase
import (
"github.com/axzilla/templui/pkg/components"
"time"
)
templ DatepickerFormats() {
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="space-y-2">
@components.Label(components.LabelProps{Text: "Default ISO", For: "datepicker-iso-format"})
@components.Datepicker(components.DatepickerProps{
ID: "datepicker-iso-format",
Config: components.DatePickerISO,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{Text: "Default EU", For: "datepicker-eu-format"})
@components.Datepicker(components.DatepickerProps{
ID: "datepicker-eu-format",
Config: components.DatePickerEU,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{Text: "UK Format", For: "datepicker-uk-format"})
@components.Datepicker(components.DatepickerProps{
ID: "datepicker-uk-format",
Config: components.DatePickerUK,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{Text: "US Format", For: "datepicker-us-format"})
@components.Datepicker(components.DatepickerProps{
ID: "datepicker-us-format",
Config: components.DatePickerUS,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{Text: "LONG Format", For: "date-long-format"})
@components.Datepicker(components.DatepickerProps{
ID: "date-long-format",
Config: components.DatePickerLONG,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{Text: "LONG Format (Spanish)", For: "date-es-long-format"})
@components.Datepicker(components.DatepickerProps{
ID: "date-es-custom-config",
Config: components.NewDatepickerConfig(
components.DateFormatLONG,
components.DateLocaleSpanish,
),
Value: time.Now().AddDate(0, 0, -30), // 30 days ago
Placeholder: "Seleccionar fecha",
})
</div>
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}
Form
Select a date from the calendar.
Please select a date
package showcase
import "github.com/axzilla/templui/pkg/components"
templ DatepickerForm() {
<div class="w-full max-w-sm">
@components.FormItem(components.FormItemProps{}) {
@components.FormLabel(components.FormLabelProps{
Text: "Select a date",
For: "datepicker-form",
})
@components.Datepicker(components.DatepickerProps{
ID: "datepicker-form",
Name: "datepicker-form",
HasError: true,
})
@components.FormDescription(components.FormDescriptionProps{}) {
Select a date from the calendar.
}
@components.FormMessage(components.FormMessageProps{
Message: "Please select a date",
Type: "error",
})
}
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"github.com/axzilla/templui/pkg/utils"
"time"
)
// DateFormat defines date formatting options
type DateFormat string
const (
DateFormatISO DateFormat = "iso" // ISO format (YYYY-MM-DD)
DateFormatEU DateFormat = "eu" // European format (DD.MM.YYYY)
DateFormatUK DateFormat = "uk" // UK format (DD/MM/YYYY)
DateFormatUS DateFormat = "us" // US format (MM/DD/YYYY)
DateFormatLONG DateFormat = "long" // Long format (Month DD, YYYY)
)
// dateFormatMapping maps DateFormat to Go time format strings
var dateFormatMapping = map[DateFormat]string{
DateFormatISO: "2006-01-02",
DateFormatEU: "02.01.2006",
DateFormatUK: "02/01/2006",
DateFormatUS: "01/02/2006",
DateFormatLONG: "January 2, 2006",
}
// DateLocale configures locale-specific settings
type DateLocale struct {
MonthNames []string // Localized month names
DayNames []string // Localized day names
}
// DateLocaleDefault and other locale presets
var DateLocaleDefault = DateLocale{
MonthNames: []string{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"},
DayNames: []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"},
}
// Pre-defined locales for different languages
var (
// DateLocaleSpanish provides Spanish localization
DateLocaleSpanish = DateLocale{
MonthNames: []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"},
DayNames: []string{"Lu", "Ma", "Mi", "Ju", "Vi", "Sa", "Do"},
}
// DateLocaleGerman provides German localization
DateLocaleGerman = DateLocale{
MonthNames: []string{"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember"},
DayNames: []string{"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"},
}
// DateLocaleFrench provides French localization
DateLocaleFrench = DateLocale{
MonthNames: []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"},
DayNames: []string{"Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"},
}
// DateLocaleItalian provides Italian localization
DateLocaleItalian = DateLocale{
MonthNames: []string{"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"},
DayNames: []string{"Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"},
}
// DateLocaleJapanese provides Japanese localization
DateLocaleJapanese = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
// DatePickerISO provides ISO format with default locale
DatePickerISO = DatepickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
// DatePickerEU provides European format with default locale
DatePickerEU = DatepickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
// DatePickerUK provides UK format with default locale
DatePickerUK = DatepickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerUS = DatepickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
// DatePickerUS provides US format with default locale
DatePickerLONG = DatepickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
// NewDatepickerConfig creates a new configuration with specified format and locale
func NewDatepickerConfig(format DateFormat, locale DateLocale) DatepickerConfig {
return DatepickerConfig{
Format: format,
Locale: locale,
}
}
func (c DatepickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO] // Default to ISO
}
// DatepickerConfig combines format and locale settings
type DatepickerConfig struct {
Format DateFormat // Date format style
Locale DateLocale // Localization settings
}
// DatepickerProps configures the Datepicker component
type DatepickerProps struct {
ID string // DOM identifier
Name string // Form field name
Value time.Time // Selected date
Config DatepickerConfig // Format and locale config
Placeholder string // Helper text shown when empty
Required bool // Marks input as mandatory
Disabled bool // Prevents interaction
HasError bool // Error state styling
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
templ DatepickerScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('datepicker', () => ({
open: false,
value: null,
format: null,
currentMonth: 5,
currentYear: new Date().getFullYear(),
monthDays: [],
blankDays: [],
months: this.$el?.dataset?.monthnames || ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
days: this.$el?.dataset?.daynames || ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
position: 'bottom',
init() {
this.format = this.$el.dataset.format;
const initialDate = this.$el?.dataset?.value ? new Date(this.parseDate(this.$el?.dataset?.value)) : new Date();
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.calculateDays();
// Format the initial value using the correct locale
if (this.$el.dataset?.value) {
this.value = this.formatDate(initialDate);
}
},
toggleDatePicker() {
this.open = !this.open;
if (this.open) {
this.$nextTick(() => this.updatePosition());
}
},
getCurrentMonth() {
return this.months[this.currentMonth] + ' ' + this.currentYear;
},
closeDatePicker() {
this.open = false;
},
updatePosition() {
const inputId = this.$root.dataset.inputId;
const trigger = document.getElementById(inputId);
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
this.position = 'top';
} else {
this.position = 'bottom';
}
},
calculateDays() {
const firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
this.blankDays = Array.from({ length: firstDay }, (_, i) => i);
this.monthDays = Array.from({ length: daysInMonth }, (_, i) => i + 1);
},
atClickPrevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.calculateDays();
},
atClickNextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.calculateDays();
},
parseDate(dateStr) {
const parts = dateStr.split(/[-/.]/);
switch(this.format) {
case 'eu':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'us':
return `${parts[2]}-${parts[0]}-${parts[1]}`;
case 'uk':
return `${parts[2]}-${parts[1]}-${parts[0]}`;
case 'long':
case 'iso':
default:
return dateStr;
}
},
formatDate(date) {
const d = date.getDate().toString().padStart(2, '0');
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const y = date.getFullYear();
switch(this.format) {
case 'eu':
return `${d}.${m}.${y}`;
case 'uk':
return `${d}/${m}/${y}`;
case 'us':
return `${m}/${d}/${y}`;
case 'long':
// Use the months array from the provided locale
return `${this.months[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isToday(day) {
const today = new Date();
const date = new Date(this.currentYear, this.currentMonth, day);
return date.toDateString() === today.toDateString();
},
isSelected(day) {
if (!this.value) return false;
const date = new Date(this.currentYear, this.currentMonth, day);
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
},
selectDate() {
const day = this.$el.getAttribute('data-day');
const date = new Date(this.currentYear, this.currentMonth, day);
this.value = this.formatDate(date);
this.open = false;
},
activeDayClass() {
const day = this.$el.getAttribute('data-day');
if (this.isSelected(day)) {
return 'bg-primary text-primary-foreground';
}
if (this.isToday(day) && !this.isSelected(day)) {
return 'text-red-500';
}
return 'hover:bg-accent hover:text-accent-foreground';
},
positionClass() {
return this.position === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1';
},
}));
});
</script>
}
}
// Datepicker renders a date selection input with calendar popup
templ Datepicker(props DatepickerProps) {
if props.ID == "" {
{{ props.ID = utils.RandomID() }}
}
if props.Placeholder == "" {
{{ props.Placeholder = "Select a date" }}
}
<div
class={ utils.TwMerge("relative", props.Class) }
if props.Value != (time.Time{}) {
data-value={ props.Value.Format(props.Config.getGoFormat()) }
}
data-format={ string(props.Config.Format) }
data-monthnames={ templ.JSONString(props.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(props.Config.Locale.DayNames) }
x-data="datepicker"
data-input-id={ props.ID }
@resize.window="updatePosition"
>
<div class="relative">
@Input(InputProps{
ID: props.ID,
Name: props.Name,
Value: props.Value.Format(props.Config.getGoFormat()),
Placeholder: props.Placeholder,
Disabled: props.Disabled,
Class: utils.TwMerge(props.Class, "peer"),
HasError: props.HasError,
Type: "text",
Readonly: true,
Attributes: utils.MergeAttributes(
templ.Attributes{
"x-ref": "datePickerInput",
":x-modelable": "value",
":value": "value",
"@click": "toggleDatePicker",
},
props.Attributes,
),
})
<button
type="button"
@click="toggleDatePicker"
disabled?={ props.Disabled }
class={
utils.TwMerge(
// Layout
"absolute top-0 right-0 px-3 py-2",
// Styling
"cursor-pointer text-muted-foreground",
// States
"hover:text-foreground",
"peer-disabled:pointer-events-none peer-disabled:opacity-50",
),
}
>
@icons.Calendar(icons.IconProps{})
</button>
</div>
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
// Layout
"absolute left-0 z-50 w-64 p-4",
// Styling
"rounded-lg border bg-popover shadow-md",
),
}
x-bind:class="positionClass"
>
<div class="flex items-center justify-between mb-4">
<span x-text="getCurrentMonth" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="atClickPrevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronLeft(icons.IconProps{})
</button>
<button
type="button"
@click="atClickNextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
>
@icons.ChevronRight(icons.IconProps{})
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<template x-for="day in days" :key="day">
<div class="text-center text-xs text-muted-foreground" x-text="day"></div>
</template>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="blank in blankDays" key="'blank' + blank">
<div class="h-8 w-8"></div>
</template>
<template x-for="day in monthDays">
<button
x-bind:data-day="day"
type="button"
@click="selectDate"
:class="activeDayClass"
x-text="day"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></button>
</template>
</div>
</div>
</div>
}