marshmallow/nova-date-range-field
marshmallow/nova-date-range-field is a Laravel Nova field that adds a date range picker for selecting start and end dates. It helps manage date ranges in Nova resources, useful for admin forms and filters.
Install the package alongside Nova (Nova itself must already be installed and configured):
composer require marshmallow/nova-date-range-field
Check the Nova constraint — this is now much clearer than before. Release 5.0.0 explicitly adds Nova 5 support (PR #5), and the package versioning now jumps to track the Nova major (1.0.6 → 5.0.0). So:
^5.0.1.0.x line.composer require picked the right major. Read vendor/marshmallow/nova-date-range-field/composer.json and confirm the laravel/nova constraint matches your installed major. The 5.0.0 release is a major bump, so Composer may refuse to upgrade an existing ^1.0 install without an explicit change.Look at the README and the field class first. Because the package is small, the field's public API is the source of truth. Open vendor/marshmallow/nova-date-range-field/src/ and confirm:
Marshmallow\NovaDateRangeField\...),start/end) or a single JSON column,->startColumn() / ->endColumn(), but verify).Your first real use case is almost always a resource with a bounded period — a booking, rental, subscription, campaign, or reporting window. Add the field to a resource's fields():
use Marshmallow\NovaDateRangeField\DateRange; // confirm namespace
DateRange::make('Booking period')
->startColumn('starts_at')
->endColumn('ends_at')
->rules('required', 'date'),
Ensure starts_at and ends_at are $fillable and cast ('date' or 'datetime') on the model, then save a record in the panel and inspect the DB row to confirm how the field actually wrote the values. Do this before writing more code — it eliminates all ambiguity about the storage model.
If the package ships uncompiled assets, run php artisan nova:publish or build the package's JS (npm install && npm run prod inside the package directory) and confirm the picker renders. If assets are precompiled, no build step is needed.
Two-column binding is the happy path. Treat the field as a UI wrapper around a start column and an end column. Keep the model simple:
protected $casts = [
'starts_at' => 'datetime',
'ends_at' => 'datetime',
];
protected $fillable = ['name', 'starts_at', 'ends_at'];
Validate the range, not just the endpoints. Field-level ->rules() often only applies to the whole field, not to each endpoint separately. Add a FormRequest (or ->rules() on a companion field) enforcing ends_at >= starts_at:
'starts_at' => ['required', 'date'],
'ends_at' => ['required', 'date', 'after_or_equal:starts_at'],
If the package's rules() doesn't reach both columns, this is the reliable guard.
Reuse via a base resource or helper method. Because the field is niche, avoid scattering configuration. Define a private method on a base Resource:
protected function periodField(string $label = 'Period'): DateRange
{
return DateRange::make($label)
->startColumn('starts_at')
->endColumn('ends_at')
->rules('required', 'date');
}
Then call $this->periodField() in each resource. This isolates any API change to one place if the package is ever replaced — and it also makes a Nova 4 → 5 upgrade a single-file touch point.
Separate the index/detail display from the form. If the field's resolveForDisplay doesn't format the range nicely, use onlyOnForms() for the date-range field and add two plain Date fields for index/detail (or a computed Text field):
$this->periodField()->onlyOnForms(),
Date::make('Starts')->onlyOnIndex(),
Date::make('Ends')->onlyOnIndex(),
For filters, prefer a hand-rolled NovaFilter. If the field can't bind to filter inputs cleanly, implement a filter with two Date inputs (from/to) and a whereBetween on the start column — this is typically more reliable than stretching a form field into a filter.
Normalize timezones on write. Use a model observer or saving hook to convert picker output to UTC (or your app's chosen storage timezone) before persisting:
static::saving(function ($model) {
$model->starts_at = $model->starts_at?->utc();
$model->ends_at = $model->ends_at?->utc();
});
Extension pattern. If you need different storage (e.g. a single JSON column), subclass the field and override fill() / resolve() rather than patching vendor code.
5.0.0 includes [BUGFIX] Undefined array key issue (PR #2), which is very likely the null/partial-range resolution crash. That is good news, but don't assume every partial case is covered: if starts_at is set and ends_at is null (or vice versa), still guard with ->displayUsing(fn () => $this->starts_at && $this->ends_at ? ... : '—') or make the field ->nullable() and show a dash. Re-test the exact null scenario from your old version against 5.0.0 before deleting any workaround.whereBetween in a filter. For any resource you'll query by date, insist on two real date columns.1.0.6 to 5.0.0 is a major jump, not a patch. An existing ^1.0 constraint will not pull in Nova 5 support. Pin explicitly per environment: "marshmallow/nova-date-range-field": "^1.0" on Nova 4, "^5.0" on Nova 5. Smoke-test after every Nova upgrade, and treat the package's own major version as a signal to re-read the changelog rather than assuming API stability.5.0.0, verify compiled assets exist under vendor/.../dist (or that the package's NovaServiceProvider/asset registration runs). Nova 5 asset registration may differ from Nova 4 — rebuild and re-publish (php artisan nova:publish) after composer update, and hard-refresh the browser cache.->rules() may not bind to both columns. Don't trust field rules for cross-column validation — put after_or_equal in a FormRequest or add an explicit saving check.POST/PUT /nova-api/... body to see exactly what the picker submits (keys, format, timezone). This is faster than reading the package source — and is the quickest way to confirm the Nova 5 upgrade didn't change the payload shape.DATETIME without normalizing, records created from different timezones will be inconsistent. Normalize in an observer and consistently display in the panel's expected zone.Date field on the same attribute. A single-column Date::make('starts_at') and this range field both writing starts_at will conflict on forms. Use onlyOnForms() / hideFrom*() to keep them mutually exclusive.Nova:: request), so a future swap is a one-file change. Also check the package license — small packages sometimes ship without one, which is a procurement issue.5.0.0 is the Nova 5 PR and the null-key bugfix. Verify both against your own install (constraint + a saved partial-range record) rather than trusting the version number alone — the jump to 5.0.0 is a compatibility declaration, not a stability guarantee.How can I help you explore Laravel packages today?