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.