Skip to content

Instantly share code, notes, and snippets.

@stephancasas
Created December 31, 2022 20:41
Show Gist options
  • Save stephancasas/4e37883edfb230fbb5cc8570a4b91007 to your computer and use it in GitHub Desktop.
Save stephancasas/4e37883edfb230fbb5cc8570a4b91007 to your computer and use it in GitHub Desktop.
A fault-tolerant Livewire $wire.entangle() alternative for AlpineJS
/**
* -----------------------------------------------------------------------------
* $rewire -- A Livewire $wire.entangle() alternative for AlpineJS
* -----------------------------------------------------------------------------
*
* $rewire provides the same functionality as the stock $wire Alpine magic, but
* adds consideration for implementations where Livewire may conditionally
* remove markup referencing undefined properties.
*
* It does this by using a regular expression pattern to check that the Livewire
* component's present markup contains an entangle() invocation referencing the
* Livewire property to be entangled.
*
* Using $rewire instead of $wire is useful when using Blade @if or @isset
* directives to prevent Livewire or Alpine from accessing a Livewire component
* property which may not always be defined and/or may not have a default value.
*
* Usage:
* Import the function into your main app.js and call it. Alpine will add the
* magic when it initializes.
*/
export default function () {
document.addEventListener("alpine:init", () => {
window.Alpine.magic("rewire", function (el) {
let wireEl = el.closest("[wire\\:id]");
if (!wireEl)
console.warn(
'Alpine: Cannot reference "$wire" outside a Livewire component.'
);
let component = wireEl.__livewire;
return {
entangle: (name, defer = false) => {
let isDeferred = defer;
let livewireProperty = name;
let livewireComponent = component;
let livewirePropertyValue = component.get(livewireProperty);
// track whether or not entanglement is via in-markup x-data
let markupPattern = new RegExp(
`\\$rewire\\.entangle\\(('|"|\`)${livewireProperty}('|"|\`)\\)`,
"g"
);
let entangledInMarkup =
!!livewireComponent.el.outerHTML.match(markupPattern);
let interceptor = window.Alpine.interceptor(
(initialValue, getter, setter, path, key) => {
// Check to see if the Livewire property exists and if not log a console error
// and return so everything else keeps running.
if (typeof livewirePropertyValue === "undefined") {
console.error(
`Livewire Entangle Error: Livewire property '${livewireProperty}' cannot be found`
);
return;
}
// Let's set the initial value of the Alpine prop to the Livewire prop's value.
let value =
// We need to stringify and parse it though to get a deep clone.
JSON.parse(
JSON.stringify(livewirePropertyValue)
);
setter(value);
// Now, we'll watch for changes to the Alpine prop, and fire the update to Livewire.
window.Alpine.effect(() => {
// if the property was entangled via x-data in-markup, check that the current markup
// still has the entanglement code -- if not, Livewire has removed it
if (
entangledInMarkup &&
!livewireComponent.el.outerHTML.match(
markupPattern
)
) {
return;
}
let value = getter();
if (
JSON.stringify(value) ==
JSON.stringify(
livewireComponent.getPropertyValueIncludingDefers(
livewireProperty
)
)
)
return;
// We'll tell Livewire to update the property, but we'll also tell Livewire
// to not call the normal property watchers on the way back to prevent another
// circular dependancy.
livewireComponent.set(
livewireProperty,
value,
isDeferred,
// Block firing of Livewire watchers for this data key when the request comes back.
// Unless it is deferred, in which cause we don't know if the state will be the same, so let it run.
isDeferred ? false : true
);
});
// We'll also listen for changes to the Livewire prop, and set them in Alpine.
livewireComponent.watch(
livewireProperty,
(value) => {
// Ensure data is deep cloned otherwise Alpine mutates Livewire data
window.Alpine.disableEffectScheduling(
() => {
setter(
typeof value !== "undefined"
? JSON.parse(
JSON.stringify(value)
)
: value
);
}
);
}
);
return value;
},
(obj) => {
Object.defineProperty(obj, "defer", {
get() {
isDeferred = true;
return obj;
},
});
}
);
return interceptor(livewirePropertyValue);
},
};
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment