Date picker
Month calendar with prev/next navigation and click-to-pick day. The grid is rendered from days() — a derived array of { day, iso, empty } objects. Selected and "today" states are decided per cell via methods, not stored as flags.
Live preview
Mo
Tu
We
Th
Fr
Sa
Su
Picked:
Markup
<div class="datepicker" data-component="datepicker-demo">
<div class="datepicker-header">
<button @click="prevMonth">‹</button>
<span data-text="title()"></span>
<button @click="nextMonth">›</button>
</div>
<div class="datepicker-weekdays">
<span>Mo</span><span>Tu</span><span>We</span>
<span>Th</span><span>Fr</span><span>Sa</span><span>Su</span>
</div>
<div class="datepicker-grid">
<template data-each="days()" data-key="iso">
<button class="datepicker-day"
@click="pick"
data-bind="data-iso:item.iso, disabled:item.empty"
data-class="empty:item.empty, selected:item.iso === selected, today:item.iso === today"
data-text="item.day"></button>
</template>
</div>
</div>
Definition
Micra.define('datepicker-demo', {
state: {
today: new Date().toISOString().slice(0, 10),
selected: '',
cursor: '', // 'YYYY-MM' — the visible month
},
onCreate() {
this.state.cursor = this.state.today.slice(0, 7)
},
// ── derived ──────────────────────────────────────────────
title() {
const [y, m] = this.state.cursor.split('-').map(Number)
return new Date(y, m - 1, 1).toLocaleString(undefined, {
month: 'long', year: 'numeric',
})
},
days() {
const [y, m] = this.state.cursor.split('-').map(Number)
const first = new Date(y, m - 1, 1)
const last = new Date(y, m, 0).getDate()
// Monday = 0, Sunday = 6
const lead = (first.getDay() + 6) % 7
const cells = []
for (let i = 0; i < lead; i++) {
cells.push({ day: '', iso: `pad-${m}-${i}`, empty: true })
}
for (let d = 1; d <= last; d++) {
const iso = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
cells.push({ day: d, iso, empty: false })
}
return cells
},
// ── actions ──────────────────────────────────────────────
prevMonth() {
const [y, m] = this.state.cursor.split('-').map(Number)
const prev = m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}`
this.state.cursor = prev
},
nextMonth() {
const [y, m] = this.state.cursor.split('-').map(Number)
const next = m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, '0')}`
this.state.cursor = next
},
pick(e) {
const iso = e.currentTarget.dataset.iso
if (!iso || iso.startsWith('pad-')) return
this.state.selected = iso
Micra.emit('date:picked', iso)
},
})
Integration
- Cursor is a string ('YYYY-MM') — survives JSON round-trips, easy to compare, and reading it never produces a stale Date object.
- Padding cells for the Monday-aligned grid carry empty: true and a synthetic iso like pad-3-0 so the keyed data-each can diff them without colliding with real dates.
- Selected + today state are computed by comparing each cell's iso to state.selected / state.today inside data-class — never written back to the day object.
- Locale-aware title. toLocaleString({ month, year }) respects the browser's locale. Set document.documentElement.lang to force a specific one.
- Bus handoff. 'date:picked' publishes the ISO string so a form, a chart, or a fetch can react.
Pitfalls
- Don't store the visible month as a Date object — its identity changes on every mutation and you risk timezone drift across toLocale* calls. Strings are stable.
- Don't precompute days into state. Cursor changes already trigger a re-render; the days method runs on read.
- Don't use $index as data-key — month-to-month transitions reorder cells and you'd lose keyed-diff benefits.