Don't let admins pick a "To" date before the "From" date Every admin panel that deals with dates hits this sooner o...
Every admin panel that deals with dates hits this sooner or later. There's a From date and a To date, sitting side by side, looking almost identical. The admin fills them in, hits save, and somewhere in the database a to_date is now earlier than a from_date. Nothing stops them. You catch it with a validation rule, they get a red error message, everyone moves on.
It works. But it's the boring kind of working. Wouldn't it be better if the second datepicker just... refused to show invalid dates?
If you're using Backpack for Laravel, you can do that with a single JavaScript file. Here's how.
Backpack PRO ships an air-datepicker field, built on top of the excellent air-datepicker library. Two fields in your controller is all it takes:
CRUD::field([
'name' => 'start_date',
'label' => 'From date',
'type' => 'air-datepicker',
]);
CRUD::field([
'name' => 'end_date',
'label' => 'To date',
'type' => 'air-datepicker',
]);
To run custom JS on the Create and Update pages, Backpack has script widgets. Add this inside setupCreateOperation() (and setupUpdateOperation() if you use it):
use Backpack\CRUD\app\Library\Widget;
Widget::add()->type('script')->content('assets/js/admin/forms/link-dates.js');
This is the whole trick. About 50 lines, and a good chunk of them are comments:
$(function () {
// Backpack initializes fields in its own ready handler, registered after
// this one — defer so the datepicker instances already exist.
setTimeout(linkDates, 0);
});
function linkDates() {
var startDp = bpFieldAirDatepickerInstance('start_date');
var endDp = bpFieldAirDatepickerInstance('end_date');
if (!startDp || !endDp) return;
// What each picker was configured with in PHP ('' by default).
var startDefaultMaxDate = startDp.opts.maxDate;
var endDefaultMinDate = endDp.opts.minDate;
// One way: "To date" can't be before "From date"...
linkAirDatepickers(startDp, endDp, 'minDate', endDefaultMinDate);
// ...and the other way around.
linkAirDatepickers(endDp, startDp, 'maxDate', startDefaultMaxDate);
}
function linkAirDatepickers(sourceDp, targetDp, limit, targetDefaultLimit) {
var originalOnSelect = sourceDp.opts.onSelect; // the field's own handler
sourceDp.opts.onSelect = function (args) {
// keep the field's built-in behavior (updates the hidden input)
originalOnSelect.apply(this, arguments);
if (args.date) {
targetDp.update({ [limit]: args.date });
// clear the target if its current selection is now invalid
var targetSelected = targetDp.selectedDates[0];
if (targetSelected && (
(limit === 'minDate' && targetSelected.getTime() < args.date.getTime()) ||
(limit === 'maxDate' && targetSelected.getTime() > args.date.getTime())
)) {
targetDp.clear();
}
} else {
// source cleared: restore the originally configured limit.
// NOTE: null throws in air-datepicker 3.6.0 — use '' instead.
targetDp.update({ [limit]: targetDefaultLimit || '' });
}
};
// apply the link on page load, when editing an entry that already has a value
var initialSource = sourceDp.selectedDates[0];
if (initialSource) {
targetDp.update({ [limit]: initialSource });
}
}
What's actually happening here?
The air-datepicker field stores its underlying datepicker instance on the input element, and exposes it through a tiny helper called bpFieldAirDatepickerInstance('field_name'). From there, the entire air-datepicker API is yours: update(), selectDate(), clear(), onSelect, you name it.
So the idea is simple: wrap the "From" picker's onSelect. When the admin picks a date, tell the "To" picker "hey, your minDate is now this". When they clear it, hand the original limit back. Do the same in reverse with maxDate, and both pickers start behaving like one well-mannered unit.
No extra packages, no validation hacks, no "please fix the dates" emails from your admins. Two fields, one script, and the form refuses to be wrong in the first place.
If you want the full explanation, we've written it up in the docs. And if you haven't tried the air-datepicker field yet, it also does datetime and range pickers, and the whole widget weighs about 13KB. Give it a spin.
Happy building.
Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.
What do you think about this?
Wondering what our community has been up to?