GrDatePicker
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
GrFormFieldwith no edits at all; - the date can be both typed and picked —
editableswitches typing on, and the parsing goes by the locale rather than by one rigid template; - some of the dates are unavailable —
disabledDatesaccepts both a list and a predicate: weekends, taken slots, “no earlier than today” throughmin/max; - something other than a
Dategoes to the backend —valueAdapterchanges 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
| Need | Take |
|---|---|
| A “from — to” period | GrDateRangePicker |
| A date together with a time | GrDateTimePicker |
| A time only | GrTimePicker |
| The month grid on its own, without a field | GrCalendar |
| Show a moment rather than choose it | GrRelativeTime |
| The choice is only among ready values, an arbitrary date is not needed | GrSelect |
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-chronoImport
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
<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
<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
<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
<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
<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
<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
<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>