GrDatePicker

Package: @feugene/granularity-chronocompanionGroup: misc

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • the date is the value of a form field — the form-control contract of the core works as a whole: the error, the requiredness and the label arrive from GrFormField with no edits at all;
  • the date can be both typed and pickededitable switches typing on, and the parsing goes by the locale rather than by one rigid template;
  • some of the dates are unavailabledisabledDates accepts both a list and a predicate: weekends, taken slots, “no earlier than today” through min/max;
  • something other than a Date goes to the backendvalueAdapter changes the type of the model to an ISO string, a timestamp or a format of your own, and that is visible in the types.

When to take something else

NeedTake
A “from — to” periodGrDateRangePicker
A date together with a timeGrDateTimePicker
A time onlyGrTimePicker
The month grid on its own, without a fieldGrCalendar
Show a moment rather than choose itGrRelativeTime
The choice is only among ready values, an arbitrary date is not neededGrSelect

The ready dates live inside the panel

presets draws a row of shortcuts in the footer: “Today”, “Tomorrow”, “Monday”. A date can be set with a function — “today” is computed at the moment of showing rather than at the moment the prop is declared.

A shortcut bypasses the grid, so it checks the prohibitions itself: a date outside min/max or from disabledDates arrives switched off — exactly like its cell in the grid. A button that does nothing deceives.

A footer of your own is the footer slot; it replaces the row as a whole and receives select, canSelect and close.

The value is local midnight rather than a moment

A date without a time is assembled as local midnight: the user chooses a day in their own calendar rather than a point on the world line. Whoever needs precisely a moment serialises it with an adapter of their own; the package will not substitute UTC silently, because “12 August” in two time zones is two different moments and one and the same day.

Not only days: a week, a month, a quarter, a year

mode is passed into the calendar as it is, and the field shows the selection in the ordinary date format:

<GrDatePicker v-model="reportPeriod" mode="quarter" />

A week puts its beginning into the model, a quarter and a month the first day, and a year the first of January. The shape of the value does not depend on the mode: there is still one date in the field, and valueAdapter works as it worked.

The details of the modes — ./GrCalendar.md; there too is why a week is drawn as a grid of days rather than as a grid of periods, and why the week number is not shown.

A set of dates is not a range

multiple collects an arbitrary set: a schedule of lessons, exception dates in a plan, booked days. The model becomes an array.

<GrDatePicker v-model="lessons" multiple />

The difference from GrDateRangePicker is one of essence rather than of convenience: there there is a continuous segment with two edges and rules about its length, here a set where adjacency means nothing.

A click on a selected date removes it. A set is a toggle rather than an accumulator: otherwise there would be nothing to remove a date taken by mistake with.

The panel does not close after a choice. A set is collected, while a single date is chosen once — and closing after the first click would turn a set of ten dates into ten openings of the panel.

The order is always ascending, wherever the clicks landed. The model has to be comparable: a rearrangement of the elements must not read as a change — otherwise “there are unsaved edits” fires out of nowhere.

Typing in this mode is switched off, even with editable. A string describing N dates requires parsing of its own: how many of them have been collected does not follow from the string, and an incomplete entry cannot be told from a short set. A range and a date with a time describe exactly two values and are therefore typeable (GrDateRangePicker, GrDateTimePicker); an arbitrary set is not.

The first three dates and the remainder as a number (“and 2 more”) are visible in the field: without a ceiling the label overflows already at the fifth. A hidden field per date goes into the form — with one name, the way FormData.getAll reads it.

min, max and disabledDates apply to every date separately, by the same rules as with a single choice.

`applyOnBlur` is switched on

A choice in the calendar is applied at once, and the loss of focus from the field commits what was typed by hand. Otherwise a date that had been typed but not confirmed would disappear on moving to the next field — while the user believes they entered it.

An editable field shows what it accepts back

With editable the value is output in digits (08/12/2026) rather than as Aug 12, 2026. Otherwise editing a number right in the field would leave the parser two groups of digits instead of three: what was typed would roll back silently, although the user broke nothing.

A format of your own overrides that rule — then the readability of what is typed is the consumer’s responsibility. Without editable the display stays as it was.

A forbidden date is not accepted as text either. disabledDates, min and max apply equally to a click and to Enter; a field bypassing the restrictions of the panel would mean there are no restrictions.

Install

npm i @feugene/granularity-chrono

Import

import { GrDatePicker } from '@feugene/granularity-chrono/components/GrDatePicker'

API

The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.

Examples 7

Basic

Basic
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<Date | null>(new Date(2026, 7, 12))
const iso = ref<string | null>('2026-08-20')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <GrDatePicker
      v-model="value"
      clearable
      placeholder="Pick a date"
      aria-label="Pick a date"
    />

    <!-- Тип модели задаёт адаптер, а не строковый проп формата: значение
         остаётся `string` и в типах, и в рантайме. -->
    <GrDatePicker
      v-model="iso"
      value-adapter="isoDate"
      :format="{ dateStyle: 'full' }"
      clearable
      placeholder="ISO model"
      aria-label="ISO model"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">Date=</span><code>{{ value?.toDateString() ?? '—' }}</code>
      <span class="opacity-70"> · isoDate=</span><code>{{ iso ?? '—' }}</code>
    </p>
  </div>
</template>

In Dialog

In Dialog
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker`, `GrDialog`, `GrButton` и `GrFormField` подставляются
// авто-импортом (`unplugin-vue-components`).
const open = ref(false)
const value = ref<string | null>('2026-08-12')
</script>

<template>
  <div class="grid gap-3">
    <GrButton class="justify-self-start" @click="open = true">
      Schedule delivery
    </GrButton>

    <GrDialog v-model="open" title="Schedule delivery" size="sm">
      <!-- Панель пикера встаёт в общий стек слоёв поверх окна: Esc закрывает
           сначала её, и только следующий — само окно. -->
      <GrFormField label="Delivery date">
        <GrDatePicker
          v-model="value"
          value-adapter="isoDate"
          clearable
          placeholder="Pick a date"
        />
      </GrFormField>

      <template #footer>
        <div class="flex items-center justify-between gap-3">
          <span class="showcase-demo-text text-sm">
            <span class="opacity-70">value=</span><code>{{ value ?? '—' }}</code>
          </span>
          <GrButton @click="open = false">
            Done
          </GrButton>
        </div>
      </template>
    </GrDialog>
  </div>
</template>

Form

Form
<script setup lang="ts">
import { computed, ref } from 'vue'

// `GrDatePicker`, `GrFormField` и `GrButton` подставляются авто-импортом.
// Модель — строка `2026-08-20`: её задаёт адаптер, а не проп формата.
const departure = ref<string | null>(null)
const attempted = ref(false)
const submitted = ref('')

const today = new Date(2026, 7, 12)

const error = computed(() => (attempted.value && !departure.value ? 'Choose a departure date' : ''))

function submit(event: Event): void {
  attempted.value = true

  // Форме уходит `2026-08-20`, а не «Aug 20, 2026»: показ локале-зависим и на
  // сервере не разбирается.
  const data = new FormData(event.target as HTMLFormElement)
  submitted.value = String(data.get('departure') ?? '')
}
</script>

<template>
  <form class="grid max-w-[320px] gap-4" @submit.prevent="submit">
    <!-- Поле пикера — обычный форм-контрол: подпись через `<label for>`,
         ошибка через `aria-describedby`, значение уходит по `name`. -->
    <GrFormField label="Departure" :error="error" required>
      <GrDatePicker
        v-model="departure"
        name="departure"
        value-adapter="isoDate"
        :min="today"
        clearable
        placeholder="Pick a date"
      />
    </GrFormField>

    <GrButton type="submit" size="sm">
      Submit
    </GrButton>

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">form data=</span><code>{{ submitted || '—' }}</code>
    </p>
  </form>
</template>

Inline

Inline
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<string | null>('2026-08-12')
</script>

<template>
  <div class="grid gap-4 justify-items-start">
    <!-- Панель на месте, поля нет — но модель, адаптер и `name` остаются
         пикеровскими: этим `inline` и отличается от голого `GrCalendar`. -->
    <GrDatePicker
      v-model="value"
      inline
      name="due"
      value-adapter="isoDate"
      aria-label="Due date"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">value=</span><code>{{ value ?? '—' }}</code>
    </p>
  </div>
</template>

Modes

Modes
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const day = ref<string | null>('2026-08-12')
const month = ref<string | null>('2026-08-01')
const year = ref<string | null>('2026-01-01')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <!-- Режим меняет и панель, и вид значения в поле: подставлять свой
         `format` для этого не нужно. -->
    <GrDatePicker v-model="day" value-adapter="isoDate" aria-label="Day" />
    <GrDatePicker v-model="month" mode="month" value-adapter="isoDate" aria-label="Month" />
    <GrDatePicker v-model="year" mode="year" value-adapter="isoDate" aria-label="Year" />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">day=</span><code>{{ day ?? '—' }}</code>
      <span class="opacity-70"> · month=</span><code>{{ month ?? '—' }}</code>
      <span class="opacity-70"> · year=</span><code>{{ year ?? '—' }}</code>
    </p>
  </div>
</template>

Multiple

Multiple
<script setup lang="ts">
import { computed, ref } from 'vue'

// `GrDatePicker` и `GrChip` подставляются авто-импортом.

/**
 * Набор дат — не диапазон.
 *
 * Демо намеренно ставит рядом список выбранного: на нём видно, что модель это
 * массив, что он всегда отсортирован и что снять дату можно двумя путями —
 * повторным кликом в сетке и крестиком в списке.
 */
const TODAY = new Date(2026, 7, 12)

const lessons = ref<Date[]>([new Date(2026, 7, 12), new Date(2026, 7, 14)])

const formatter = new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: 'short' })

const chips = computed(() => lessons.value.map(date => ({
  key: date.toISOString().slice(0, 10),
  label: formatter.format(date),
  date,
})))

function remove(key: string): void {
  lessons.value = lessons.value.filter(date => date.toISOString().slice(0, 10) !== key)
}
</script>

<template>
  <div class="grid gap-4 justify-items-start">
    <GrDatePicker
      v-model="lessons"
      multiple
      :today="TODAY"
      locale="ru-RU"
      placeholder="Выберите занятия"
      aria-label="Даты занятий"
      class="w-80"
    />

    <div v-if="chips.length > 0" class="flex flex-wrap items-center gap-2">
      <!-- Удаление у чипа своё: `closable` плюс `remove`, свой крестик был бы копией. -->
      <GrChip
        v-for="chip in chips"
        :key="chip.key"
        size="sm"
        closable
        :remove-label="`Убрать ${chip.label}`"
        @remove="remove(chip.key)"
      >
        {{ chip.label }}
      </GrChip>
    </div>

    <p v-else class="showcase-demo-text text-sm opacity-70">
      Пока ничего не выбрано.
    </p>

    <p class="showcase-demo-text text-sm opacity-70">
      Наберите несколько дат: панель <strong>не закрывается</strong> — набор набирают, а одиночную
      дату выбирают однажды. Клик по выбранной снимает её: набор это переключатель, а не накопитель,
      иначе снять ошибочно взятую дату было бы нечем.
    </p>

    <p class="showcase-demo-text text-sm opacity-70">
      Порядок в модели всегда по возрастанию, куда бы вы ни кликнули — модель обязана быть сравнима,
      иначе перестановка читалась бы как изменение. В поле видны первые три даты и остаток числом:
      без потолка подпись переполняется уже на пятой. Отличие от диапазона — в существе: там
      непрерывный отрезок с двумя краями, здесь множество, где соседство ничего не значит.
    </p>
  </div>
</template>

Typed

Typed
<script setup lang="ts">
import { ref } from 'vue'

// `GrDatePicker` подставляется авто-импортом (`unplugin-vue-components`).
const value = ref<string | null>('2026-08-12')
const russian = ref<string | null>('2026-08-12')
</script>

<template>
  <div class="grid max-w-[320px] gap-4">
    <!-- Порядок частей и разделитель берутся из локали, поэтому маска и
         подсказка формата у полей разные, а проп — один. -->
    <GrDatePicker
      v-model="value"
      editable
      value-adapter="isoDate"
      locale="en-US"
      clearable
      aria-label="US format"
    />

    <GrDatePicker
      v-model="russian"
      editable
      value-adapter="isoDate"
      locale="ru-RU"
      clearable
      aria-label="RU format"
    />

    <p class="showcase-demo-text text-sm">
      <span class="opacity-70">en=</span><code>{{ value ?? '—' }}</code>
      <span class="opacity-70"> · ru=</span><code>{{ russian ?? '—' }}</code>
    </p>
  </div>
</template>

Component documentationAll components