Created
December 31, 2022 20:41
-
-
Save stephancasas/4e37883edfb230fbb5cc8570a4b91007 to your computer and use it in GitHub Desktop.
A fault-tolerant Livewire $wire.entangle() alternative for AlpineJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ----------------------------------------------------------------------------- | |
* $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