skip to content

How do you integrate a third-party date-picker widget into a Livewire booking form so re-renders don't break it and the date reaches PHP?

level: middleimportance: must knowfreq 45%

answer

  1. morph would undo the widget's DOM
  2. wire:ignore on the wrapper
  3. @assets loads the library once
  4. script block runs per component instance
  5. $wire.checkIn = date, or $watch back

basics

~20 s

Wrap the picker's markup in wire:ignore so Livewire's DOM morph leaves it alone, load the library with @assets, initialise it in the component's script, and push each selected date into $wire.checkIn; watch that property to reflect server changes.

solid answer

~40 s

A date-picker library rewrites the DOM it is given, and Livewire morphs the component's HTML after every request, so without protection each update strips or resets the widget. Put its container under `wire:ignore`, which tells the morph to skip that element's children. Load the library with `@assets`, which Livewire injects once per page even for lazily loaded components, and initialise it in the component's script: a root-level `<script>` in a single-file component, or `@script ... @endscript` in a class-based view. There, find the input with `$wire.$el.querySelector()`, write selections to `$wire.checkIn` (or `$wire.$set()` when availability must refresh immediately), and use `$wire.$watch('checkIn', ...)` to update the picker when PHP changes the date, because the morph no longer will.

code

html · 21 lines
html
<form wire:submit="book">
    <div wire:ignore>
        <input type="text" data-picker value="{{ $checkIn }}">
    </div>
    @error('checkIn') <p>{{ $message }}</p> @enderror
    <button type="submit">Book</button>
</form>

@assets
<script src="/vendor/datepicker/datepicker.js" defer></script>
@endassets

@script
<script>
    const picker = createDatePicker($wire.$el.querySelector('[data-picker]'), {
        onChange: (date) => { $wire.checkIn = date },
    })

    $wire.$watch('checkIn', (value) => picker.setDate(value))
</script>
@endscript

go deeper

for a junior

Remember wire:ignore: it stops Livewire's re-render from touching the widget's markup, and you still have to send the chosen date to $wire yourself.

for a middle

Explain the three pieces, wire:ignore, @assets and the component script, and how the value flows both ways with $wire and $watch.

for a senior

Handle the hard parts: server-driven resets, multiple instances, clean-up on removal, lazy and wire:navigate loading, and keeping validation on the server.

for a principal

Decide whether the team wraps common widgets once as reusable Livewire or Blade components with a standard contract, rather than wiring each form by hand.

## Why widgets and Livewire collide A **Livewire component** re-renders its Blade view on every request and **morphs** the result into the page: it walks the old and new DOM and patches the differences. A third-party **date-picker library** does its own DOM work: it wraps the input, adds a calendar element, sets attributes, and keeps internal state tied to those nodes. After the next Livewire update, the morph sees DOM that the Blade view never produced and "fixes" it, which removes the calendar, resets classes or detaches event listeners. Three Livewire features solve this, and each has one job. ## 1. `wire:ignore`: keep the morph out ```html <div wire:ignore> <input type="text" data-picker> </div> ``` Everything **inside** an element marked `wire:ignore` is skipped by the morph, even if the server-rendered markup for it changes. The modifier `wire:ignore.self` is narrower: it ignores only the element's own attribute changes and still morphs its children. For a widget that rewrites its subtree, use plain `wire:ignore` on a wrapper. The cost: Livewire no longer updates anything in that subtree. If PHP changes the date, the input will not show it unless you push it in yourself. ## 2. `@assets`: load the library once ```html @assets <script src="/vendor/datepicker/datepicker.js" defer></script> <link rel="stylesheet" href="/vendor/datepicker/datepicker.css"> @endassets ``` Livewire injects `@assets` content into the page the first time any instance of the component appears and never again, however many instances render. It works for components that arrive later through lazy loading or `wire:navigate`, and Livewire loads assets before it runs the component's scripts. ## 3. The component script: initialise per instance In a **single-file** Livewire 4 component, a `<script>` at the root of the template is the component's script; a **class-based** component wraps it in `@script ... @endscript`. Either way it runs once each time an instance enters the page, with `$wire` bound to that instance, and not again on later updates. ## Wiring the value both ways - **Picker to PHP**: in the picker's change callback, assign `$wire.checkIn = date`. The value is queued and sent with the next request, such as the form's submit. If choosing a date should immediately refresh room availability, call `$wire.$set('checkIn', date)` instead, or call an action. - **PHP to picker**: `$wire.$watch('checkIn', value => picker.setDate(value))` keeps the widget in sync when the server resets or changes the date, since the ignored subtree will not be morphed. - **The `wire:model` route**: if the library updates the real input and dispatches an `input` event, `wire:model="checkIn"` on that input can work without JavaScript glue. Many libraries only set the value silently, which is why the explicit `$wire` write is the reliable path. ## The Alpine-wrapper variant Some teams prefer to hold the widget in an Alpine component instead of a component script, because Alpine gives it a clear lifetime: ```html <div wire:ignore x-data="{ picker: null, init() { this.picker = createDatePicker(this.$refs.input, { onChange: d => { this.$wire.checkIn = d } }) }, destroy() { this.picker.destroy() } }"> <input x-ref="input" type="text"> </div> ``` `init()` runs when Alpine initialises the element, `$refs.input` scopes the lookup to this instance, and `destroy()` runs when the element leaves the DOM, which is where a library's own tear-down belongs. The same pattern is easy to extract into an anonymous Blade component so every form that needs a date picker gets it with one tag. ## Clean-up and pitfalls 1. **Scope DOM queries** with `$wire.$el.querySelector()`, not `document.querySelector()`, so two booking forms on one page do not grab each other's inputs. 2. **Destroy the widget** when the component is removed if the library holds global listeners; an Alpine `x-data` wrapper with a `destroy()` method is a tidy home for that. 3. **Do not put `wire:ignore` on the whole form**: validation messages and other fields would stop updating. 4. **Keep server validation**: the picker's min and max dates are UX, not rules; the check-in date is still validated in PHP.

  • After adding wire:ignore, a Livewire action that clears $checkIn no longer clears the picker. Why, and how do you fix it?
    `wire:ignore` stops Livewire from morphing anything inside the wrapper, so the server's new empty value never reaches the input. Push it in yourself: `$wire.$watch('checkIn', value => picker.setDate(value))` in the component script, or dispatch a browser event from PHP that the script listens for.
  • Why use @assets instead of a script tag inside the Livewire component's markup to load the library?
    A tag in the markup would be re-evaluated for each instance and can be lost or duplicated as the component re-renders. `@assets` is injected once per page, including for components that arrive later through lazy loading or `wire:navigate`, and Livewire loads it before running the component's scripts.

saying these in an interview costs you the question

  • wire:ignore.self is the right choice for a widget that rewrites its children.
  • Livewire leaves any DOM a third-party library created untouched on re-render.
  • The component script runs again after every Livewire update.
  • Putting wire:ignore on the whole form is the simplest safe fix.
  • Setting the picker's allowed dates removes the need for server validation.