Date Picker
Calendar interface for selecting and formatting dates.
TailwindCSS
Alpine.js
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerDefault() {
<div class="w-full max-w-sm">
@components.DatePicker()
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}
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/components"
templ DatePickerWithLabel() {
<div class="w-full max-w-sm space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-with-label",
}) {
Pick a date
}
@components.DatePicker(components.DatePickerProps{
ID: "date-picker-with-label",
})
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}
Custom Placeholder
package showcase
import "github.com/axzilla/templui/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/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}
Selected Date
package showcase
import (
"github.com/axzilla/templui/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/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}
Disabled
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerDisabled() {
<div class="w-full max-w-sm">
@components.DatePicker(components.DatePickerProps{
Disabled: true,
})
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}
Formats
package showcase
import (
"github.com/axzilla/templui/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{
For: "date-picker-iso-format",
}) {
Default ISO format
}
@components.DatePicker(components.DatePickerProps{
ID: "date-picker-iso-format",
Config: components.DatePickerISO,
Value: time.Now(),
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-eu-format",
}) {
Default EU format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerEU,
Value: time.Now(),
ID: "date-picker-eu-format",
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-uk-format",
}) {
UK Format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerUK,
Value: time.Now(),
ID: "date-picker-uk-format",
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-picker-us-format",
}) {
US Format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerUS,
Value: time.Now(),
ID: "date-picker-us-format",
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-long-format",
}) {
LONG Format
}
@components.DatePicker(components.DatePickerProps{
Config: components.DatePickerLONG,
Value: time.Now(),
ID: "date-long-format",
})
</div>
<div class="space-y-2">
@components.Label(components.LabelProps{
For: "date-es-long-format",
}) {
LONG Format (Spanish)
}
@components.DatePicker(components.DatePickerProps{
Config: components.NewDatePickerConfig(
components.DateFormatLONG,
components.DateLocaleSpanish,
),
Value: time.Now().AddDate(0, 0, -30), // 30 days ago
Placeholder: "Seleccionar fecha",
ID: "date-es-long-format",
})
</div>
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}
Form
Select a date from the calendar.
Select a valid date
package showcase
import "github.com/axzilla/templui/components"
templ DatePickerForm() {
<div class="w-full max-w-sm">
@components.FormItem() {
@components.FormLabel(components.FormLabelProps{
For: "date-picker-form",
}) {
Select a date
}
@components.DatePicker(components.DatePickerProps{
ID: "date-picker-form",
HasError: true,
})
@components.FormDescription() {
Select a date from the calendar.
}
@components.FormMessage(components.FormMessageProps{
Variant: components.FormMessageVariantError,
}) {
Select a valid date
}
}
</div>
}
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"time"
"strconv"
)
type DateFormat string
const (
DateFormatISO DateFormat = "iso"
DateFormatEU DateFormat = "eu"
DateFormatUK DateFormat = "uk"
DateFormatUS DateFormat = "us"
DateFormatLONG DateFormat = "long"
)
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",
}
type DateLocale struct {
MonthNames []string
DayNames []string
}
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"},
}
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 = 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 = 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 = 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 = DateLocale{
MonthNames: []string{"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月"},
DayNames: []string{"日", "月", "火", "水", "木", "金", "土"},
}
)
var (
DatePickerISO = DatePickerConfig{
Format: DateFormatISO,
Locale: DateLocaleDefault,
}
DatePickerEU = DatePickerConfig{
Format: DateFormatEU,
Locale: DateLocaleDefault,
}
DatePickerUK = DatePickerConfig{
Format: DateFormatUK,
Locale: DateLocaleDefault,
}
DatePickerUS = DatePickerConfig{
Format: DateFormatUS,
Locale: DateLocaleDefault,
}
DatePickerLONG = DatePickerConfig{
Format: DateFormatLONG,
Locale: DateLocaleDefault,
}
)
func NewDatePickerConfig(format DateFormat, locale DateLocale) DatePickerConfig {
return DatePickerConfig{
Format: format,
Locale: locale,
}
}
type DatePickerConfig struct {
Format DateFormat
Locale DateLocale
}
type DatePickerProps struct {
ID string
Class string
Attributes templ.Attributes
Value time.Time
Config DatePickerConfig
Placeholder string
Disabled bool
Required bool
HasError bool
Name string
}
templ DatePicker(props ...DatePickerProps) {
{{ var p DatePickerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
if p.Placeholder == "" {
{{ p.Placeholder = "Select a date" }}
}
<div
id={ p.ID + "-container" }
class={ utils.TwMerge("relative", p.Class) }
if p.Value != (time.Time{}) {
data-value={ p.Value.Format(p.Config.getGoFormat()) }
}
data-format={ string(p.Config.Format) }
data-monthnames={ templ.JSONString(p.Config.Locale.MonthNames) }
data-daynames={ templ.JSONString(p.Config.Locale.DayNames) }
data-placeholder={ p.Placeholder }
x-data="datePicker"
@resize.window="updatePosition"
{ p.Attributes... }
>
<input
type="hidden"
id={ p.ID + "-hidden" }
name={ p.Name }
x-ref="hiddenInput"
disabled?={ p.Disabled }
required?={ p.Required }
/>
<div class="relative">
@Button(ButtonProps{
ID: p.ID,
Type: "button",
Variant: ButtonVariantOutline,
Class: utils.TwMerge(
"w-full select-trigger flex items-center justify-between focus:ring-2 focus:ring-offset-2",
utils.If(p.HasError, "border-destructive ring-destructive"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"@click": "toggleDatePicker",
"x-ref": "triggerButton",
"required": strconv.FormatBool(p.Required),
},
p.Attributes,
),
}) {
<span
x-ref="displayText"
class="text-left grow"
>
if p.Value != (time.Time{}) {
{ p.Value.Format(p.Config.getGoFormat()) }
} else {
<span class="text-muted-foreground">{ p.Placeholder }</span>
}
</span>
<!-- Calendar icon -->
<span class="text-muted-foreground flex items-center">
@icons.Calendar()
</span>
}
</div>
<!-- DatePicker Popup -->
<div
x-show="open"
x-ref="datePickerPopup"
@click.away="closeDatePicker"
x-transition.opacity
class={
utils.TwMerge(
"absolute left-0 z-50 w-64 p-4",
"rounded-lg border bg-popover shadow-md",
),
}
style="display: none;"
>
<div class="flex items-center justify-between mb-4">
<span x-ref="monthDisplay" class="text-sm font-medium"></span>
<div class="flex gap-1">
<button
type="button"
@click="prevMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
<button
type="button"
@click="nextMonth"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden 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()
</button>
</div>
</div>
<!-- Weekday container -->
<div x-ref="weekdaysContainer" class="grid grid-cols-7 gap-1 mb-2"></div>
<!-- Calendar days container -->
<div x-ref="daysContainer" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
}
func (c DatePickerConfig) getGoFormat() string {
if format, ok := dateFormatMapping[c.Format]; ok {
return format
}
return dateFormatMapping[DateFormatISO]
}
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: new Date().getMonth(),
currentYear: new Date().getFullYear(),
monthNames: [],
dayNames: [],
position: 'bottom',
init() {
// Parse Month and Day names
try {
this.monthNames = JSON.parse(this.$el.dataset.monthnames) || [];
this.dayNames = JSON.parse(this.$el.dataset.daynames) || [];
if (!this.monthNames.length) {
this.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
}
if (!this.dayNames.length) {
this.dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
}
// Format and Placeholder
this.format = this.$el.dataset.format || 'iso';
this.placeholder = this.$el.dataset.placeholder || 'Select a date';
// Process initial value
const initialValue = this.$el.dataset.value;
if (initialValue) {
const initialDate = new Date(this.parseDate(initialValue));
this.currentMonth = initialDate.getMonth();
this.currentYear = initialDate.getFullYear();
this.value = this.formatDate(initialDate);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
// Update display text
this.updateDisplayText();
}
// Initialize UI
this.updateMonthDisplay();
this.renderWeekdays();
} catch(e) {
console.error('DatePicker initialization error:', e);
}
},
updateDisplayText() {
const displayTextEl = this.$refs.displayText;
if (!displayTextEl) return;
displayTextEl.innerHTML = '';
if (this.value) {
displayTextEl.textContent = this.value;
// Remove muted style
const muted = displayTextEl.querySelector('.text-muted-foreground');
if (muted) {
muted.classList.remove('text-muted-foreground');
}
} else {
const span = document.createElement('span');
span.className = 'text-muted-foreground';
span.textContent = this.placeholder;
displayTextEl.appendChild(span);
}
},
toggleDatePicker() {
if (this.$refs.triggerButton.disabled) return;
this.open = !this.open;
if (this.open) {
this.$nextTick(() => {
this.updatePosition();
this.renderCalendar();
});
}
},
closeDatePicker(event) {
const trigger = this.$refs.triggerButton;
if (event.target.tagName === 'LABEL' && event.target.htmlFor === trigger.id) {
return;
}
this.open = false;
},
updateMonthDisplay() {
if (this.$refs.monthDisplay) {
this.$refs.monthDisplay.textContent = this.monthNames[this.currentMonth] + ' ' + this.currentYear;
}
},
updatePosition() {
const trigger = this.$refs.triggerButton;
const popup = this.$refs.datePickerPopup;
if (!trigger || !popup) return;
setTimeout(() => {
const rect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (rect.bottom + popupRect.height > viewportHeight && rect.top > popupRect.height) {
popup.classList.add('bottom-full', 'mb-1');
popup.classList.remove('top-full', 'mt-1');
} else {
popup.classList.add('top-full', 'mt-1');
popup.classList.remove('bottom-full', 'mb-1');
}
}, 0); // 0ms is enough to wait for the next tick
},
renderWeekdays() {
const container = this.$refs.weekdaysContainer;
if (!container) return;
container.innerHTML = '';
// Day headers
this.dayNames.forEach(day => {
const el = document.createElement('div');
el.className = 'text-center text-xs text-muted-foreground';
el.textContent = day;
container.appendChild(el);
});
},
renderCalendar() {
// Calculate first day and days in month
let firstDay = new Date(this.currentYear, this.currentMonth, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1; // Sunday = 6
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
const today = new Date();
const container = this.$refs.daysContainer;
if (!container) return;
container.innerHTML = '';
// Empty days at the beginning
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.className = 'h-8 w-8';
container.appendChild(blank);
}
// Days in the month
for (let day = 1; day <= daysInMonth; day++) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium';
button.textContent = day;
button.dataset.day = day;
// Click handler
button.addEventListener('click', e => this.selectDate(e));
// Highlight today or selected
const isToday = today.getDate() === day &&
today.getMonth() === this.currentMonth &&
today.getFullYear() === this.currentYear;
const isSelected = this.isSelectedDate(day);
if (isSelected) {
button.classList.add('bg-primary', 'text-primary-foreground');
} else if (isToday) {
button.classList.add('text-red-500');
} else {
button.classList.add('hover:bg-accent', 'hover:text-accent-foreground');
}
container.appendChild(button);
}
},
prevMonth() {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
this.updateMonthDisplay();
this.renderCalendar();
},
nextMonth() {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
this.updateMonthDisplay();
this.renderCalendar();
},
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':
return `${this.monthNames[date.getMonth()]} ${d}, ${y}`;
case 'iso':
default:
return `${y}-${m}-${d}`;
}
},
isSelectedDate(day) {
if (!this.value) return false;
try {
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
const selected = new Date(this.parseDate(this.value));
return date.toDateString() === selected.toDateString();
} catch(e) {
return false;
}
},
selectDate(e) {
const day = e.target.dataset.day;
if (!day) return;
const date = new Date(this.currentYear, this.currentMonth, parseInt(day));
this.value = this.formatDate(date);
// Update hidden input
this.$refs.hiddenInput.value = this.value;
this.$refs.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
// Update display text
this.updateDisplayText();
// Close DatePicker
this.open = false;
// Focus on button
this.$refs.triggerButton.focus();
}
}));
});
</script>
}
}