Scoping Reactive Object References In The DOM In Alpine.js 3
From what I can gather, Alpine.js works by maintaining a stack of reactive objects in the background; and then, attaches those reactive objects to the DOM tree via "expando" properties. When you then reference a value within an Alpine.js expression, Alpine.js walks up the DOM tree, finds the closest expando property, locates the appropriate reactive object, and evaluates your expression. Essentially, Alpine.js is creating a sort of "prototype chain" for its data bindings.
Aside: An "expando" property is any non-standard property that is added to the Document Object Model (DOM) via JavaScript. This is an age-old technique for storing "state" in the DOM.
This nested object context makes it easy to get up-and running with Alpine.js; but, I wonder if it might lead to confusion as to where data is actually defined (especially in the CSP edition, which requires that all data be defined externally to the DOM). Coming from an AngularJS background, I can attest that "scope inheritance" became a point of contention within the Angular community (and was eventually removed in Angular 2+).
As a thought experiment, I wanted to see if I could create a custom Alpine.js directive that would allow the developer to provide an explicit scope for a given x-data
binding. Then, this scope could be used within an Alpine.js expression to clearly identify which x-data
binding was being consumed.
If this were implemented natively in Alpine.js framework, perhaps it could be defined as a directive "value" (ie, the part of the directive notation that comes after the :
). In the following snippet, I'm using the me
scope:
<div x-data:me="{ name: 'Ben' }">
<span x-text="me.name"></span>
</div>
As you can see, I'm scoping the x-data
binding as me
; and then, I'm using me.name
to reference to the name
property.
Of course, this isn't implemented natively. So, for this thought experiment, I'm going to provide a sibling directive, x-scope
, which takes the scope label as an attribute expression:
<div x-data="{ name: 'Ben' }" x-scope="me">
<span x-text="me.name"></span>
</div>
Internally, you can think of this x-scope
directive as performing this assignment:
reactiveScope[ "me" ] = reactiveScope;
Essentially, all I want to do is create an additional property on the reactive scope that points back to itself.
Here's my attempt at an implementation. In this extremely trite example, I have two nested x-data
bindings that each expose a value
property. Then, I have buttons that each target one of the scoped values:
<!doctype html>
<html lang="en">
<body>
<!--
THIS IS A CONTRIVED EXAMPLE that isn't realistic. But, it illustrates a potential
upside in being able to explicitly scope which reactive object you are referencing
within the DOM markup. Here, I'm using the "x-scope" directive to provide a prefix
for the "x-data" instance that I want to consume.
-->
<div x-data="{ value: 0 }" x-scope="outer">
<div x-data="{ value: 0 }" x-scope="inner">
<button @click="outer.value++">
<strong>Outer:</strong>
<span x-text="outer.value"></span>
</button>
<button @click="inner.value++">
<strong>Inner:</strong>
<span x-text="inner.value"></span>
</button>
</div>
</div>
<script type="text/javascript" src="../vendor/alpine.3.13.5.min.js" defer></script>
<script type="text/javascript">
document.addEventListener(
"alpine:init",
function setupAlpineBindings() {
Alpine.directive( "scope", ScopeDirective );
}
);
function ScopeDirective( element, metadata, framework ) {
// Get the reactive scope from the current element.
var datastack = framework.Alpine.closestDataStack( element );
var reactiveScope = datastack[ 0 ];
// Supply a reference back to THIS (reactive scope proxy) using label.
reactiveScope[ metadata.expression ] = reactiveScope;
}
</script>
</body>
</html>
As you can see, both Divs have x-data="{ value: 0 }"
bindings. And, both buttons are nested with the inner Div. However, one button references outer.value
while the other button references inner.value
, where outer
and inner
are the explicitly provided scopes.
And, when we run this Alpine.js code, we get the following output:
As you can see, even though both buttons are nested inside the inner Div, each button is able to reference, increment, and render the appropriate value
property thanks to the x-scope
alias. Without the alias scoping, both buttons would have mutated the inner Div's value
property.
I'm an Alpine.js n00b; so, it's very possible that this kind of mechanic isn't actually needed. I can only say that in the AngularJS world, it was helpful and cut down on bugs. And, if nothing else, it's just teaching me more about how Alpine.js works.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →