Интеграция календаря Pikaday в современные фронтенд-стэки почти всегда приводит к необходимости изоляции императивного DOM-API библиотеки от декларативной модели UI-фреймворков. Wrapper-компоненты решают задачу согласования жизненного цикла, состояния и событий, превращая внешний datepicker в предсказуемый управляемый элемент интерфейса.
Ключевая идея wrapper-компонента заключается в том, что экземпляр Pikaday создаётся, обновляется и уничтожается внутри контролируемого контейнера, а наружу экспортируется только синхронизированное состояние даты и события изменения.
Wrapper вокруг Pikaday обычно состоит из трёх логических уровней:
Внутри большинства UI-фреймворков Pikaday остаётся императивным объектом:
const picker = new Pikaday({
field: inputElement,
format: 'YYYY-MM-DD',
onSelect: function(date) {
state.value = date;
}
});
Wrapper-компонент инкапсулирует эту конструкцию и добавляет контроль над обновлением входных данных и уничтожением экземпляра при размонтировании.
В React wrapper строится на использовании useEffect и
useRef. Основная задача — предотвратить повторную
инициализацию и обеспечить корректное уничтожение экземпляра.
import { useEffect, useRef } from "react";
import Pikaday from "pikaday";
function DatePicker({ value, onChange }) {
const inputRef = useRef(null);
const pickerRef = useRef(null);
useEffect(() => {
pickerRef.current = new Pikaday({
field: inputRef.current,
defaultDate: value,
setDefaultDate: true,
onSelect: (date) => {
onChange(date);
}
});
return () => {
pickerRef.current.destroy();
};
}, []);
useEffect(() => {
if (pickerRef.current && value) {
pickerRef.current.setDate(value, true);
}
}, [value]);
return <input ref={inputRef} />;
}
setDatedestroy() при unmountWrapper может поддерживать два режима:
Контролируемый режим требует постоянного вызова setDate,
что важно учитывать при производительности.
В Vue wrapper чаще всего реализуется через компонент или директиву.
import Pikaday from "pikaday";
export default {
props: {
modelValue: Date
},
emits: ["update:modelValue"],
mounted() {
this.picker = new Pikaday({
field: this.$refs.input,
onSelect: (date) => {
this.$emit("update:modelValue", date);
}
});
if (this.modelValue) {
this.picker.setDate(this.modelValue);
}
},
watch: {
modelValue(newVal) {
if (this.picker) {
this.picker.setDate(newVal);
}
}
},
unmounted() {
this.picker.destroy();
}
};
Директива обеспечивает более лёгкую интеграцию в существующие input-элементы:
export default {
mounted(el, binding) {
el._pikaday = new Pikaday({
field: el,
defaultDate: binding.value,
setDefaultDate: true,
onSelect(date) {
binding.instance.$emit("update:modelValue", date);
}
});
},
updated(el, binding) {
el._pikaday.setDate(binding.value);
},
unmounted(el) {
el._pikaday.destroy();
}
};
watchv-modelВ Angular интеграция Pikaday строится вокруг формы API и интерфейса
ControlValueAccessor.
import { Directive, ElementRef, forwardRef, HostListener } from '@angular/core';
import Pikaday from 'pikaday';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Directive({
selector: '[appDatepicker]',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatepickerDirective),
multi: true
}]
})
export class DatepickerDirective implements ControlValueAccessor {
private picker: any;
constructor(private el: ElementRef) {}
ngOnInit() {
this.picker = new Pikaday({
field: this.el.nativeElement,
onSelect: (date: Date) => {
this.onChange(date);
}
});
}
writeValue(value: Date): void {
if (this.picker) {
this.picker.setDate(value);
}
}
registerOnChange(fn: any): void {
this.onCha nge = fn;
}
registerOnTouched(fn: any): void {
this.onTouc hed = fn;
}
onCha nge = (date: Date) => {};
onTouc hed = () => {};
ngOnDestroy() {
if (this.picker) {
this.picker.destroy();
}
}
}
ngOnInit и
ngOnDestroyВ более старых архитектурах Pikaday часто оборачивается в jQuery-плагин для унификации API.
(function($) {
$.fn.pikadayWrapper = function(options) {
return this.each(function() {
const $el = $(this);
const picker = new Pikaday({
field: this,
...options
});
$el.data('pikaday', picker);
});
};
})(jQuery);
Особенности:
.data()Ключевая проблема wrapper-архитектуры — рассинхронизация состояния. Pikaday управляет DOM напрямую, тогда как UI-фреймворки работают с виртуальной моделью.
1. Event-driven
onSelect2. Controlled model
setDate3. Hybrid
Wrapper часто становится местом, где централизуется работа с форматами дат:
YYYY-MM-DD)new Pikaday({
field: input,
format: 'DD.MM.YYYY',
i18n: {
previousMonth: 'Предыдущий месяц',
nextMonth: 'Следующий месяц',
months: [...],
weekdays: [...]
}
});
Wrapper может дополнительно:
Wrapper-компоненты критически важны для предотвращения утечек памяти.
Основные риски:
destroy()Стандартный lifecycle:
В зрелых кодовых базах wrapper обычно выносится в отдельный слой:
createDatePicker(input, options)usePikaday(ref, config)withDatepicker(Component)Пример универсального hook:
function usePikaday(inputRef, value, onChange) {
const pickerRef = useRef(null);
useEffect(() => {
pickerRef.current = new Pikaday({
field: inputRef.current,
onSelect: onChange
});
return () => pickerRef.current.destroy();
}, []);
useEffect(() => {
if (pickerRef.current) {
pickerRef.current.setDate(value);
}
}, [value]);
return pickerRef.current;
}
Wrapper слой часто становится точкой расширения функциональности:
new Pikaday({
field,
minDate: new Date(2024, 0, 1),
maxDate: new Date(2026, 11, 31),
disableDayFn: (date) => date.getDay() === 0
});
Wrapper может добавлять:
Wrapper-компоненты превращают Pikaday из изолированной DOM-библиотеки в управляемый элемент современной UI-системы, обеспечивая предсказуемость состояния, контроль жизненного цикла и совместимость с реактивными моделями данных.