Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Shannon Cross
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Shannon Cross

Scoping Reactive Object References In The DOM In Alpine.js 3

By on

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:

When user clicks buttons, the relevant values are being incremented.

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel