karnoweb/livewire-datepicker
Jalali and Gregorian datepicker for Livewire 3 and Alpine.js with wire:model support for single, range, and multiple dates, min/max limits, themes, RTL, and Gregorian export for storage. No npm or Vite setup required.
karnoweb/livewire-datepicker is a Blade component (<x-jalali-datepicker />) plus two PHP conversion helpers. It is not a JS package you wire up yourself — jalaali-js ships inside the package and is served automatically, so there is no npm install, no Vite entry, and no Alpine plugin registration.
1. Install with the right release line. This is the first thing to get right, because the two lines are not interchangeable:
# Laravel 11–12, PHP 8.2+
composer require karnoweb/livewire-datepicker:^1.0
# Laravel 13, PHP 8.3+
composer require karnoweb/livewire-datepicker:^13.0
2. Publish the config (optional but do it on day one, so defaults are visible and reviewable in your repo):
php artisan vendor:publish --tag=datepicker-config
config/datepicker.php holds default_calendar, formats.input, formats.export, month/weekday names, theme, and first_day_of_week — the four settings that cause most "why does it look wrong" reports.
3. Where to look first, in order:
config/datepicker.php — defaults for input/export format and first day of week.Karnoweb\LivewireDatepicker\jalali_to_gregorian() / gregorian_to_jalali() — you will use these constantly for display and import, not just for the picker.--tag=datepicker-views) only if you must change markup.4. First use case — persist Gregorian, display Jalali. This is the package's headline feature and the pattern almost every app wants: a date column in MySQL, a Persian UI.
// Livewire component
public $birth_date = null;
protected $rules = [
'birth_date' => 'required|date_format:Y-m-d|before:today',
];
<x-jalali-datepicker
wire:model="birth_date"
label="تاریخ تولد"
placeholder="انتخاب تاریخ"
export-calendar="gregorian"
export-format="Y-m-d"
:required="true"
/>
Because the property now holds 1990-01-01 rather than 1368/10/11, ordinary Laravel validation and Eloquent date casting work untouched. Ship this on one admin form first; that form becomes your team's reference implementation.
House wrapper component (do this before your second usage). Rather than repeating four attributes on every call site, wrap it once in Blade so defaults live in one place and can be changed package-wide:
{{-- resources/views/components/app/date.blade.php --}}
<x-jalali-datepicker
{{ $attributes->merge([
'export-calendar' => 'gregorian',
'export-format' => 'Y-m-d',
]) }}
/>
Call sites become <x-app.date wire:model="birth_date" label="..." />. If you ever swap libraries, you edit one file.
Edit forms: seed the property, not the default attribute. default is a static Blade attribute in export format; on an edit screen the value already comes from the bound model. Assign in mount() and leave default for create forms that need a suggested date.
Range filters. wire:model receives { start, end }, which maps cleanly onto a query:
public $date_range = [];
public function updatedDateRange(): void
{
$this->validate([
'date_range.start' => 'nullable|date_format:Y-m-d',
'date_range.end' => 'nullable|date_format:Y-m-d|after_or_equal:date_range.start',
]);
}
public function render()
{
return view('livewire.orders', [
'orders' => Order::query()
->when($this->date_range['start'] ?? null, fn ($q, $d) => $q->whereDate('created_at', '>=', $d))
->when($this->date_range['end'] ?? null, fn ($q, $d) => $q->whereDate('created_at', '<=', $d))
->paginate(),
]);
}
Use wire:model.live on a filter bar so the table re-queries on selection; use plain wire:model (deferred) inside forms so you get one request on submit instead of one per click.
Multiple selection. The property arrives as an array; cast it and use whereIn:
protected $casts = ['selected_dates' => 'array'];
<x-jalali-datepicker wire:model="selected_dates" :multiple="true" :max-selections="5" />
max-selections is client-side guidance — still validate array|max:5 server-side.
Dependent dates. The picker knows nothing about your business rules, so drive min-date from state instead of hard-coding it:
<x-app.date wire:model="checkin" :min-date="$todayJalali" />
<x-app.date wire:model="checkout" :min-date="$checkinJalali" />
Compute $checkinJalali in the component with gregorian_to_jalali() and format it to match input-format (Y/m/d by default) — remember min/max use the input format, not the export format.
Server-side formatting for non-picker surfaces. Tables, invoices, and emails still need Jalali strings. Use the helpers, and note they return arrays, not Carbon:
use function Karnoweb\LivewireDatepicker\gregorian_to_jalali;
$j = gregorian_to_jalali($order->created_at->year, $order->created_at->month, $order->created_at->day);
$label = sprintf('%04d/%02d/%02d', $j['year'], $j['month'], $j['day']);
Wrap this in your own Jalali::format() helper — it will be called hundreds of times and you do not want the package namespace scattered through views.
Bulk import from Jalali data. jalali_to_gregorian($y, $m, $d) returns ['year','month','day']; combine with Carbon::create(...)->toDateString() before insert so your columns stay Gregorian and sortable.
Testing. Do not simulate the calendar UI. Set the exported value directly and assert:
Livewire::test(CreatePatient::class)
->set('birth_date', '1990-01-01')
->call('save')
->assertHasNoErrors();
Add a single browser/Dusk test for "calendar opens and a day commits a value" to cover the Alpine integration, and keep the rest as fast component tests.
export-calendar defaults to same. Forget it and your date column receives 1403/01/01, which fails date_format:Y-m-d and silently breaks Carbon::parse downstream. If a Jalali string is landing in the database, this is the cause nine times out of ten. Set export-calendar="gregorian" explicitly, even when it matches the config default.
Two formats, two audiences. input-format governs the visible input and the strings you pass to min-date, max-date, and disabled-dates. export-format governs what wire:model receives and what default expects. Mixing them produces calendars with no explanation for why a date is greyed out or unreachable. Keep input-format="Y/m/d" and export-format="Y-m-d" and treat that as a project convention.
The value shape changes with the mode. Single → string; range → object with start/end; multiple → array. Validation rules and updated* hooks must match, or you get "Array to string conversion" the first time someone switches a field to range mode.
Duplicate Alpine kills it silently. The input renders, but clicking never opens the calendar. Livewire ships Alpine; if your layout also loads Alpine (a stray CDN script or an old plugin bundle), the component's x-data never initialises. This is the top debugging check: calendar doesn't open → look for a second Alpine, then for a CSP blocking the package's auto-served script.
Always wire:key pickers rendered in a loop. Without it Livewire reuses DOM nodes and one row's calendar shows another row's month:
<x-app.date wire:key="date-{{ $row->id }}" wire:model="rows.{{ $row->id }}.date" />
Don't publish assets unless you intend to rebuild. --tag=datepicker-assets copies raw JS to public/vendor/datepicker and you will then be serving a stale bundle after every composer update. The auto-served bundled script is the correct default.
Theme auto follows prefers-color-scheme, not your app's dark-mode class. If your UI toggles a .dark class manually, pass theme="dark" / theme="light" from a computed property so the picker matches the rest of the page.
first_day_of_week must be 6 for Persian weeks. A Saturday-start calendar configured with 0 looks subtly wrong every single day — the kind of bug users report as "the dates are off by one."
holidays in the config is reserved for future use. Don't build on it. Generate :disabled-dates="[...]" from your own holidays table, formatted to input-format.
Timezone discipline. The picker is date-only. Store date columns, not datetime, or you will chase off-by-one-day bugs from UTC conversion. If you need a time, add a separate time input and combine server-side.
Version and maintenance risk. Zero stars, one maintainer, and a hard fork in the release line based on Laravel version. Pin the exact minor (^1.0.3), and keep all package contact behind your x-app.date wrapper plus a Jalali helper class. morilog/jalali is already a transitive dependency and exposes the same conversions, so a swap to it (or to a first-party conversion) is a contained afternoon rather than a codebase-wide search-and-replace.
Debugging checklist, in order: calendar won't open → duplicate Alpine or blocked script; wrong value in the property → export-calendar / export-format; wrong dates disabled → input-format mismatch on min-date/max-date; wrong weekday column → first_day_of_week; stale behaviour after upgrading the package → a previously published asset copy in public/vendor/datepicker.
How can I help you explore Laravel packages today?