Using Skirbi with Livewire

3 min

@skirbi/semtic and @skirbi/pinta are built to survive being morphed by Livewire (and by Alpine’s own morph, which Livewire uses under the hood). A Skirbi component renders itself once, on connect, into real HTML; a Livewire re-render then morphs the DOM around it rather than replacing it wholesale, so component state (open/closed, focus, scroll position) isn’t lost just because a wire:model changed somewhere on the page.

This is the same “components are resistant to morphing” principle from Why Skirbi; this guide is the practical side of it, for a real Livewire app rather than the design rationale.

Morph control attributes

@skirbi/sugar’s withConnectedSucu mixin (which every morph-aware Skirbi component uses) recognizes a small set of attributes that let you override its default morph behaviour per instance:

  • morph-ignore – don’t observe this element for morphs at all. Use it on a component whose own internal state should never be touched by a Livewire re-render (a modal that’s mid-animation, a panel a user is actively editing inside).
  • morph-child, morph-attribute, morph-subtree – narrow what’s observed instead of ignoring it outright: child-list changes only, attribute changes only, or the whole subtree. Reach for these when the default (child-list only) isn’t matching what Livewire is actually changing.
  • morph-debug – logs every observed mutation for that element to the console. Turn it on when a component isn’t reacting the way you expect and you need to see what Livewire’s morph is actually doing to it.

Overriding a component’s template

Every Skirbi component looks for a <template id="..."> matching its own template ID before falling back to the one it ships with (see HtmlTemplate in @skirbi/sugar’s HTMLElementSugar). That means a project can override a component’s internal structure – including adding morph-ignore to specific nodes inside it – without forking the component itself, just by shipping a <template> with the right id earlier in the page.

For example, overriding <dibuho-container-panel>’s template to keep its header out of the morph-observed set:

html
<template id="dibuho-container-panel-template">
  <semtic-panel type="card" dibuho-container-panel>
    <semtic-stack dibuho-container-panel-stack>
      <semtic-header morph-ignore dibuho-container-panel-header></semtic-header>
      <semtic-stack dibuho-container-panel-body></semtic-stack>
    </semtic-stack>
  </semtic-panel>
</template>

Every <dibuho-container-panel> on the page then renders from this template instead of its built-in one – no change to the component, no extra JS, just an inline <template>.

Binding wire:model

A Blade component wrapping a Skirbi input just needs to keep whatever wire:model* attribute was passed in and put it on the real control Livewire needs to bind to (typically an <input>/<select>/<textarea> inside the component, reachable via a [wc-control] hook):

blade
@props(['name' => null])

@php
  $model = $attributes->whereStartsWith('wire:model')->first();
@endphp

<semtic-input name="{{ $name }}">
  <input wc-control {{ $model ? "wire:model=$model" : '' }}>
</semtic-input>

The component itself doesn’t know or care about Livewire; it only cares that the control it renders is a real, bindable form element.

See also