GrDatePicker
Берут, когда дата — значение поля формы.
Когда брать
- дата — значение поля формы — контракт форм-контрола ядра работает целиком:
ошибка, обязательность и подпись приезжают из
GrFormFieldбез единой правки; - дату можно и набрать, и выбрать —
editableвключает ввод с клавиатуры, разбор идёт по локали, а не по одному жёсткому шаблону; - часть дат недоступна —
disabledDatesпринимает и список, и предикат: выходные, занятые слоты, «не раньше сегодня» черезmin/max; - на бэкенд уходит не
Date—valueAdapterменяет тип модели на строку ISO, timestamp или свой формат, и это видно в типах.
Когда взять другое
| Нужно | Берите |
|---|---|
| Период «с — по» | GrDateRangePicker |
| Дата вместе со временем | GrDateTimePicker |
| Только время | GrTimePicker |
| Сетка месяца сама по себе, без поля | GrCalendar |
| Показать момент, а не выбрать его | GrRelativeTime |
| Выбирают только из готовых значений, произвольная дата не нужна | GrSelect |
Готовые даты живут внутри панели
presets рисует ряд шорткатов в подвале: «Сегодня», «Завтра», «Понедельник».
Дату можно задать функцией — «сегодня» считается в момент показа, а не в момент
объявления пропа.
Шорткат идёт мимо сетки, поэтому проверяет запреты сам: дата вне min/max или
из disabledDates приходит выключенной — ровно как её ячейка в сетке. Кнопка,
которая ничего не делает, обманывает.
Свой подвал — слот footer; он заменяет ряд целиком и получает select,
canSelect и close.
Значение — локальная полночь, а не момент
Дата без времени собирается как локальная полночь: пользователь выбирает день в своём календаре, а не точку на мировой линии. Кому нужен именно момент — тот сериализует его своим адаптером; молча подставлять UTC пакет не станет, потому что «12 августа» в двух часовых поясах это два разных момента и один и тот же день.
Не только дни: неделя, месяц, квартал, год
mode пробрасывается в календарь как есть, и поле показывает выбранное обычным форматом даты:
<GrDatePicker v-model="reportPeriod" mode="quarter" />
Неделя кладёт в модель своё начало, квартал и месяц — первое число, год — первое января. Форма
значения от режима не зависит: в поле по-прежнему одна дата, и valueAdapter работает как работал.
Разбор режимов — ./GrCalendar.md; там же о том, почему неделя рисуется сеткой
дней, а не сеткой периодов, и почему номер недели не показывается.
Набор дат — не диапазон
multiple собирает произвольное множество: расписание занятий, даты-исключения в графике,
забронированные дни. Модель становится массивом.
<GrDatePicker v-model="lessons" multiple />
Отличие от GrDateRangePicker — в существе, а не в удобстве: там
непрерывный отрезок с двумя краями и правилами длины, здесь — множество, где соседство ничего не
значит.
Клик по выбранной дате её снимает. Набор — переключатель, а не накопитель: иначе снять ошибочно взятую дату было бы нечем.
Панель не закрывается после выбора. Набор набирают, а одиночную дату выбирают однажды — и закрытие после первого клика превратило бы набор из десяти дат в десять открытий панели.
Порядок всегда по возрастанию, куда бы ни кликнули. Модель обязана быть сравнима: перестановка элементов не должна читаться как изменение — иначе «есть несохранённые правки» срабатывает на пустом месте.
Ручной ввод в этом режиме выключен, даже при editable. Строка, описывающая N дат, требует
своего разбора: сколько их набрано, из строки не следует, а неполный ввод не отличить от короткого
набора. Диапазон и дата со временем описывают ровно два значения, и потому набираются
(GrDateRangePicker, GrDateTimePicker); произвольный набор — нет.
В поле видны первые три даты и остаток числом («и ещё 2»): без потолка подпись переполняется уже на
пятой. В форму уходит по скрытому полю на дату — с одним именем, как читает FormData.getAll.
min, max и disabledDates действуют на каждую дату по отдельности, теми же правилами, что и при
одиночном выборе.
`applyOnBlur` включён
Выбор в календаре применяется сразу, а уход фокуса из поля фиксирует набранное руками. Иначе набранная, но не подтверждённая дата исчезала бы при переходе к следующему полю — а пользователь считает, что он её ввёл.
Редактируемое поле показывает то, что принимает обратно
С editable значение выводится цифрами (08/12/2026), а не Aug 12, 2026.
Иначе правка числа прямо в поле оставляла бы разбору две группы цифр вместо
трёх: набранное молча откатывалось бы, хотя пользователь ничего не нарушил.
Свой format это правило перебивает — тогда за читаемость набранного отвечает
потребитель. Без editable показ остаётся прежним.
Запрещённая дата не принимается и текстом. disabledDates, min и max
действуют одинаково на клик и на Enter; поле, обходящее ограничения панели,
означало бы, что ограничений нет.
Установка
npm i @feugene/granularity-chronoИмпорт
import { GrDatePicker } from '@feugene/granularity-chrono/components/GrDatePicker'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 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>