components/bootstrap-datetimepicker
Composer-ready shim for Eonasdan's Bootstrap DateTimePicker, adding date and time picker widgets to Bootstrap forms with Moment.js. Easily install and manage picker assets and dependencies in PHP projects.
This is not a Laravel package. There is no Composer requirement, no service provider, no publishable config, and no Blade component — smalot/bootstrap-datetimepicker is a front-end jQuery plugin from 2016. "Installing" it means putting two asset files into your build (or public/) and writing the initializer yourself. Your day-to-day leverage comes entirely from how well you wrap that initializer.
Prerequisites you must have already: jQuery and Bootstrap 3 CSS (the widget ships glyphicon arrows), plus a build step (Vite or Mix). If the project isn't already on Bootstrap 3 + jQuery, don't start here — see the exit notes in Gotchas.
Install and pin:
npm install --save-exact [email protected] jquery
Then wire the global in a separate module imported first (this ordering matters — see gotchas):
// resources/js/jquery-global.js
import $ from 'jquery';
window.$ = window.jQuery = $;
// resources/js/app.js
import './jquery-global';
import 'bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js';
import 'bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css';
Where to look first: node_modules/bootstrap-datetimepicker/build/ for the dist files and build/js/locales/, and the README's option table. Because the project is archived, the source is the documentation for anything the README omits — budget for reading it.
First use case — a single published_at field on a create/edit form:
<input type="text" name="published_at" id="published_at"
class="form-control js-datetimepicker"
autocomplete="off"
value="{{ old('published_at', optional($post->published_at)->format('Y-m-d H:i')) }}">
$('.js-datetimepicker').datetimepicker({
format: 'yyyy-mm-dd hh:ii', // PHP-style tokens, not moment tokens
autoclose: true,
minuteStep: 1,
todayBtn: true,
pickerPosition: 'bottom-left',
});
Note the format string: this fork uses PHP-ish tokens (yyyy, mm, dd, hh, ii, ss), which maps almost 1:1 onto Carbon::format(). That is one of the few places where it is friendlier to a Laravel backend than its successor.
| PHP / Carbon | Plugin |
|---|---|
Y |
yyyy |
m |
mm |
d |
dd |
H |
hh |
i |
ii |
s |
ss |
One Blade partial, one option set. Never scatter datetimepicker({...}) calls through views. Make one anonymous component that owns the markup and the format:
{{-- resources/views/components/datetime-field.blade.php --}}
@props(['name', 'value' => null, 'mode' => 'datetime'])
@php
$format = $mode === 'date' ? 'yyyy-mm-dd' : 'yyyy-mm-dd hh:ii';
@endphp
<input type="text" {{ $attributes->merge(['class' => 'form-control js-datetimepicker']) }}
name="{{ $name }}" id="{{ $name }}" autocomplete="off"
data-dp-format="{{ $format }}"
data-dp-mode="{{ $mode }}"
value="{{ old($name, $value) }}">
Lazy-init via delegation is the single highest-value pattern here. It survives Livewire morphs, dynamically added rows, and Inertia/Turbo page swaps without any re-init bookkeeping:
const defaults = {
autoclose: true,
minuteStep: 1,
todayBtn: true,
pickerPosition: 'bottom-left',
forceParse: false, // server validates; don't silently rewrite user input
};
document.addEventListener('focusin', (e) => {
const el = e.target.closest('.js-datetimepicker');
if (!el || el.dataset.dpReady) return;
el.dataset.dpReady = '1';
const opts = { ...defaults, format: el.dataset.dpFormat || 'yyyy-mm-dd hh:ii' };
if (el.dataset.dpMode === 'date') Object.assign(opts, { minView: 2, maxView: 3 });
$(el).datetimepicker(opts).on('changeDate', function () {
// bubble a native event so wire:model / Alpine / your own listeners see it
this.dispatchEvent(new Event('input', { bubbles: true }));
});
});
Livewire. Wrap the input in wire:ignore so the morph doesn't fight the picker's DOM, keep wire:model, and let the input dispatch above do the syncing. For dynamic row components, the focusin delegation means you write zero extra code.
Edit forms. Repopulate with the exact same format string you gave the widget, or it will parse-fail and blank the field:
'value' => $post->published_at?->format('Y-m-d H:i')
Same for old() — after a validation failure the browser posts back whatever the widget wrote, so the accepted format must be byte-identical to the picker format.
Validation parity. Keep the server rule in step with the widget:
$data = $request->validate([
'published_at' => ['nullable', 'date_format:Y-m-d H:i'],
'expires_at' => ['nullable', 'date_format:Y-m-d H:i', 'after:published_at'],
]);
With forceParse: false a user can type garbage and submit it, so the FormRequest is not optional. Also decide timezone explicitly: the picker produces browser wall-clock time, so either convert on the way in (Carbon::createFromFormat('Y-m-d H:i', $value, $request->user()->timezone)->utc()) or render in config('app.timezone') and accept the mismatch. A datetime cast on the model plus UTC storage is the low-friction path.
Coupled start/end fields — use the methods rather than re-instantiating:
$('#starts_at').on('changeDate', function () {
$('#ends_at').datetimepicker('setStartDate', this.value || null);
});
Other methods worth knowing: 'show', 'hide', 'update' (set value programmatically), and 'remove' (unbind — use this before destroying a row).
Testing. Dusk can't type() into a readonly input, and clicking calendar cells is brittle. Set the value through the API and submit:
$browser->script("$('#published_at').datetimepicker('update', '2024-05-01 09:30');");
Fork confusion is the #1 time sink. There are two near-identical libraries: this one (smalot, options like format: 'yyyy-mm-dd hh:ii', minView, startDate, language, event changeDate) and eonasdan/bootstrap-datetimepicker → Tempus Dominus (format: 'YYYY-MM-DD HH:mm', viewMode, minDate, locale, events dp.change). Most Stack Overflow answers, blog posts, and AI completions are for the other one. Symptoms: options silently ignored, dates rendering as yyyy-mm-dd literally, event handlers never firing. When something "doesn't work," check which library the snippet came from before debugging anything else.
jQuery must exist before the plugin module executes. ES module imports are hoisted and evaluated in source order, so import 'jquery-global' above import 'bootstrap-datetimepicker' is load-bearing. Getting this wrong produces $(...).datetimepicker is not a function only in the production bundle.
minuteStep defaults to 5. Anyone entering 09:32 gets silently snapped. Set it to 1 unless the domain genuinely wants quarter-hours.
Events fire more than you think. changeDate also fires while the user navigates months/years. Don't treat it as "selection committed" — commit on hide (with autoclose: true), or check e.date first. If you're dispatching to Livewire on every changeDate, expect extra round-trips.
initialDate does not populate the field. It only sets where the calendar opens when the input is empty. The input's value still has to come from Blade.
Locales are separate script files, and the names don't match Laravel's. build/js/locales/bootstrap-datetimepicker.fr.js registers itself into $.fn.datetimepicker.dates['fr']; app()->getLocale() may be pt_BR while the file is pt-BR. Import only the locales you ship and map explicitly:
const map = { fr_FR: 'fr', pt_BR: 'pt-BR', en_US: 'en' };
const lang = map[document.documentElement.lang] || 'en';
Clipping inside modals and scroll containers. The picker is positioned next to the input, so an overflow: hidden parent or a scrolling modal cuts it off. Mitigate with pickerPosition: 'bottom-left', remove the offending overflow, and re-run positioning on shown.bs.modal. There is no container option in this fork.
Bootstrap 3 icon dependency. On Bootstrap 4/5 the glyphicon-arrow-* classes resolve to nothing and the arrows vanish. Either keep the BS3 icon font loaded for these widgets or add a small CSS override mapping .icon-arrow-left/right to your icon set.
Guard against double-init. Calling .datetimepicker() twice on the same element stacks pickers. That's what the data-dp-ready flag above is for; use 'remove' if you truly need to re-create.
Archived means you own it. Pinned version + committed lockfile or nothing. There will be no upstream fix for a browser quirk; the practical maintenance model is "vendor the dist files into public/vendor/datetimepicker/ and never touch them again." Also note the transitive cost: this ships no dependency of its own, but every consumer of it already drags in jQuery, and moment is legacy. Neither should be introduced into a new Laravel 11/12 + Vite + Tailwind codebase.
Accessibility is poor. No meaningful ARIA, partial keyboard support, and a focus-trapping-free popup. If the audience includes screen-reader users, this widget will fail an audit regardless of how well you configure it.
When to stop using it. For anything new — or if you're mid-migration to Bootstrap 4/5 or Inertia + Vue/React — use flatpickr (no jQuery, i18n via imports, Vite-friendly, altInput hides the display value) or, for the simplest forms, a native <input type="datetime-local"> validated server-side with date_format:Y-m-d\TH:i. Both beat hand-maintaining an archived widget. Keep smalot only where the app is already committed to jQuery + Bootstrap 3 and the picker already works; there, the wrapper pattern above is cheap and stable, and a migration is a "someday" item, not a today item.
Extension points, if you must. Locale strings live in $.fn.datetimepicker.dates, so you can register a custom language inline instead of shipping a file; styling is plain CSS against the widget's class names; and because the plugin mutates DOM directly, any behavior change is easiest applied by listening to changeDate/hide rather than patching the prototype.
How can I help you explore Laravel packages today?