Various Ways To Get ColdFusion Data Into An Alpine.js Component
So far, all of my Alpine.js explorations have been client-side focused. But, my ultimate goal is to see if Alpine.js is a good companion framework for a ColdFusion-based multi-page application (MPA). As such, I wanted to spend some time thinking about various ways in which to get my ColdFusion data into an Alpine.js component.
In this exploration, I'm assuming that the requested ColdFusion page is already fetching the data on the server-side. As such, none of these examples rely on fetch()
—or any other API-based workflow—in order to gather data. All of these example assume that there is already a request.users
array ready to render.
To make that assumption true, I've created an ColdFusion application framework component that initializes some sample data at the top of each request:
component {
// Define the application settings.
this.name = "AlpineJsDataDemo";
this.sessionManagement = false;
this.setClientCookies = false;
// ---
// LIFE-CYCLE METHODS.
// ---
/**
* I initialize the request.
*/
public void function onRequestStart() {
request.users = [
[ id: 1, name: "Kimmie" ],
[ id: 2, name: "Ricki" ],
[ id: 3, name: "Bobbi" ],
[ id: 4, name: "Sammi" ]
];
}
}
Each of the following examples will render this array of users.
Pull Directly From a Global Variable
In this example, we're using the x-for
Alpine.js directive to render the list of users. The x-for
directive uses a for-in
syntax that references an array. And, in this case the array will be a globally-accessible JavaScript variable:
x-for="user in globalData"
This globalData
variable isn't stored in an Alpine component. Instead, we're using a <script>
tag to define a global variable that represents our serialized ColdFusion data.
<!doctype html>
<html lang="en">
<body>
<h1>
Pull Directly From Global Variable
</h1>
<main x-data>
<ul>
<template x-for="user in globalData" :key="user.id">
<li>
<strong x-text="user.id"></strong>:
<span x-text="user.name"></span>
</li>
</template>
</ul>
<!--
CAUTION: This button does NOT WORK because the global data variable is not a
reactive variable. As such, Alpine has no idea that a new item is added.
-->
<button
@click="
globalData.push({
id: 5,
name: 'Noobi'
});
console.dir( globalData )
">
Add User
</button>
</main>
<script type="text/javascript" src="../vendor/alpine.3.13.5.js" defer></script>
<script type="text/javascript">
// Store the users in a global variable (ie, one implicitly scoped to Window).
// --
// SECURITY NOTE: I'm using JSON.parse() + encodeForJavaScript() here as a best
// practice to make sure that I don't open myself up to a persisted Cross-Site
// Scripting (XSS) attack. This will ensure that any special characters that have
// a malicious intent are escaped HTML parsing.
var globalData = JSON.parse( "<cfoutput>#encodeForJavaScript( serializeJson( request.users ) )#</cfoutput>" );
</script>
</body>
</html>
Regardless of Alpine.js, moving data from a server-side ColdFusion context into a client-side JavaScript context requires a serialization / deserialization workflow across the wire. Such a workflow can open the door to a security vulnerability (such as a persisted cross-site scripting attack). As such, we have to take-care when serializing the data into the rendered page.
In this case, I'm using the encodeForJavaScript()
ColdFusion function to make sure that any malicious data, embedded within the user data, is escaped / deactivated within the JavaScript context. Then, I'm using the JSON.parse()
JavaScript method to deserialize this stringified data back into a native JavaScript array.
This creates the globalData
variable, which is what I'm then referencing in my x-for
Alpine.js directive. And, when we run this ColdFusion / Alpine.js code, we get the following output:
As you can see, the list of ColdFusion users is rendered by Alpine.js.
This demo also includes a button to add a new user. And, when clicked, you can see that the new user data shows up in the global data variable (logged to the console); but, not in the user interface. This is because I'm mutating the global variable directly ( via .push()
).
Alpine.js relies on a reactive proxy approach to data reconciliation. As such, when I change data that's "outside" of the Alpine.js boundary, Alpine.js doesn't know that anything has changed. To fix this, we need to move the global data into a reactive context.
Map Global Variable Onto a Reactive Scope Value
This next example is almost exactly the same as the previous example. Only, instead of referencing a global variable from within our x-for
directive, we're going to map the global variable onto a scoped data value. All this does is allow Alpine.js to create a reactive proxy wrapper around the global data which allows Alpine to react to changes in the data structure:
<!doctype html>
<html lang="en">
<body>
<h1>
Map Global Variable Onto Reactive Scope Value
</h1>
<main x-data="app">
<ul>
<template x-for="user in users" :key="user.id">
<li>
<strong x-text="user.id"></strong>:
<span x-text="user.name"></span>
</li>
</template>
</ul>
<!--
This time, this button WILL WORK because the scoped "users" value is a
reactive wrapper around the global data variable. As such, any time we push
data onto the proxied array, Alpine knows about it and updates the DOM.
-->
<button
@click="
users.push({
id: 5,
name: 'Noobi'
});
console.dir( users )
">
Add User
</button>
</main>
<script type="text/javascript" src="../vendor/alpine.3.13.5.js" defer></script>
<script type="text/javascript">
document.addEventListener(
"alpine:init",
function setupAlpineBindings() {
Alpine.data( "app", AppController );
}
);
// Store the users in a global variable (ie, one implicitly scoped to Window).
// --
// SECURITY NOTE: I'm using JSON.parse() + encodeForJavaScript() here as a best
// practice to make sure that I don't open myself up to a persisted Cross-Site
// Scripting (XSS) attack. This will ensure that any special characters that have
// a malicious intent are escaped during HTML parsing.
var globalData = JSON.parse( "<cfoutput>#encodeForJavaScript( serializeJson( request.users ) )#</cfoutput>" );
/**
* I control the root app component.
*/
function AppController() {
return {
// Wrapping raw data in reactive, proxy data.
users: globalData
};
}
</script>
</body>
</html>
This time, instead of this x-for
directive:
x-for="user in globalData"
... we have this x-for
directive:
x-for="user in users"
This simple alias is enough to make the data rendering reactive. Which is why, this time, when we click the "Add User" button, we update both the global data and the user interface:
As you can see in the console logging, we're not working with a native array. Instead, we have a Proxy
object that targets our globalData
array. This proxy is what allows Alpine.js to see the .push()
operation; and, update the x-for
rendering in response.
Pass Global Variable Into Component Constructor
In the previous examples, the rendering of the x-for
Alpine.js directive is tightly coupled to the existence of the globalData
variable. In the first example, we referenced it directly in the HTML; and, in the second example, we referenced it directly in the app component. This creates a high degree of coupling between our Alpine.js components and our serialization / deserialization workflow.
To reduce some of this coupling, we can pass the globalData
value into the constructor function for the Alpine.js component. We're still referencing the globalData
value; but, the Alpine.js components no longer need to know where this data is coming from:
<!doctype html>
<html lang="en">
<body>
<h1>
Pass Global Variable Into Component Constructor
</h1>
<!-- Passing globalData into app() constructor as a parameter. -->
<main x-data="app( globalData )">
<ul>
<template x-for="user in users" :key="user.id">
<li>
<strong x-text="user.id"></strong>:
<span x-text="user.name"></span>
</li>
</template>
</ul>
<button
@click="
users.push({
id: 5,
name: 'Noobi'
});
console.dir( globalData )
">
Add User
</button>
</main>
<script type="text/javascript" src="../vendor/alpine.3.13.5.js" defer></script>
<script type="text/javascript">
document.addEventListener(
"alpine:init",
function setupAlpineBindings() {
Alpine.data( "app", AppController );
}
);
// Store the users in a global variable (ie, one implicitly scoped to Window).
// --
// SECURITY NOTE: I'm using JSON.parse() + encodeForJavaScript() here as a best
// practice to make sure that I don't open myself up to a persisted Cross-Site
// Scripting (XSS) attack. This will ensure that any special characters that have
// a malicious intent are escaped during HTML parsing.
var globalData = JSON.parse( "<cfoutput>#encodeForJavaScript( serializeJson( request.users ) )#</cfoutput>" );
/**
* I control the root app component.
*/
function AppController( rawData ) {
return {
// Wrapping raw data in reactive, proxy data.
users: rawData
};
}
</script>
</body>
</html>
Notice our x-data
directive hook:
x-data="app( globalData )"
The x-data
directive doesn't just give us a way to attach a component constructor to a given DOM element, it gives us a way to pass initialization data into said constructor. In this case, we're passing globalData
into the app constructor; and, the app constructor is then including that argument data into its scoped array data.
This essentially recreates the previous example; but, breaks the tight-coupling of the app
component to the location of the globalData
variable. And, when we run this ColdFusion / Alpine.js code, we get the same output as before:
Clicking the button adds a new user; and, since this user data is being added to the reactive proxy, Alpine.js updates both the underlying globalData
variable and the user interface. I'm not bothering to show the console logging since it's the same as the previous example.
x-data
Scope Binding
Serialize Data Directly Into the In the previous example, I'm serializing the ColdFusion data into a global JavaScript variable. But, if our needs are simple enough, we can actually skip the global variable altogether and serialize the data directly into the x-data
attribute:
<!doctype html>
<html lang="en">
<body>
<h1>
Serialize Data Directly Into x-data Scope Binding
</h1>
<main x-data="{
users: JSON.parse( '<cfoutput>#encodeForJavaScript( serializeJson( request.users ) )#</cfoutput>' )
}">
<ul>
<template x-for="user in users" :key="user.id">
<li>
<strong x-text="user.id"></strong>:
<span x-text="user.name"></span>
</li>
</template>
</ul>
<!--
This time, this button WILL WORK because the data was used to directly define
an x-data scope.
-->
<button
@click="
users.push({
id: 5,
name: 'Noobi'
});
console.dir( users )
">
Add User
</button>
</main>
<script type="text/javascript" src="../vendor/alpine.3.13.5.js" defer></script>
</body>
</html>
As you can see, instead of referencing an Alpine.js component constructor, our x-data
attribute defines the reactive scope directly within the HTML. Again, we're using our encodeForJavaScript()
ColdFusion function in conjunction with the JSON.parse()
JavaScript method to make sure that our code is secure against an XSS attack. And, when we run this ColdFusion / Alpine.js code, we get the following output:
As you can see, the ColdFusion data has been serialized into the JSON.parse()
call which is embedded directly in the x-data
attribute markup. This showcases the fact that all of these Alpine.js directives are evaluating native JavaScript code. Meaning, the scope data isn't defined using some sort of DSL (Domain Specific Language)—it's quite literally an object literal; and, the values in the object literal are evaluated JavaScript expressions.
Render Data Directly With ColdFusion
In all of the previous examples, we've been using the x-for
directive to take the array of users and render it on the client-side using a <template>
definition. But, there's no reason that we have to rely on client-side templating. We have ColdFusion - a server-side rendering platform. So, why not just render the list of users directly on the server.
In this example, we're going to use the <cfloop>
tag to output the list of users on the server-side in ColdFusion. Which means, this demo isn't strictly the same as the previous demos. Instead of creating an Alpine.js component for the overall "list" of users, we're going to create an Alpine.js component for each "list item".
<!doctype html>
<html lang="en">
<body>
<h1>
Render Data Directly With ColdFusion
</h1>
<main x-data>
<ul>
<cfoutput>
<!--
Instead of using x-for to render the data, we can just render it with
ColdFusion on the server-side. Then, we can attack Alpine.js bindings
to the individual rows instead of the overall list.
-->
<cfloop index="user" array="#request.users#">
<li
x-data="userRow"
data-user-id="#encodeForHtmlAttribute( user.id )#">
<strong>#encodeForHtml( user.id )#</strong>:
<span>#encodeForHtml( user.name )#</span>
<button @click="deleteUser">
Delete
</button>
</li>
</cfloop>
</cfoutput>
</ul>
</main>
<script type="text/javascript" src="../vendor/alpine.3.13.5.js" defer></script>
<script type="text/javascript">
document.addEventListener(
"alpine:init",
function setupAlpineBindings() {
Alpine.data( "userRow", UserController );
}
);
/**
* I control the user row component.
*/
function UserController() {
// NOTE: We could have passed this in as a constructor argument; but, for the
// sake of variety, I'm accessing it via the dataset in order to demonstrate
// a DOM-oriented source of truth.
var userID = +this.$el.dataset.userId;
console.log( "User row:", userID );
return {
deleteUser: function() {
// TODO: AJAX call to server to delete actual data.
console.log( "Deleting:", userID );
// In this context, the $el is the event target, NOT the element on
// which the x-data binding was defined.
this.$el.parentElement.remove();
}
};
}
</script>
</body>
</html>
Using a server-rendered list changes what it means to make this page interactive. For example, it's harder now to just add a new user since we're no longer using client-side templating. But, we can still attach an x-data
directive to each user list item; and, attempt to make the page more interactive at the user level.
I'm not actually making any API calls, since that is beyond the scope of the post. But, you can see that I've exposed a deleteUser()
method on each list item controller. And, when we click the "Delete" button, we can see that said handler is invoked:
In this case, we're using Alpine.js less to render the data and more to just attach event-handlers to the ColdFusion-rendered DOM. And, that's OK. There's no rule that says data has to be managed in any particular way. The only rule is to do whatever makes the most sense for your given set of constraints.
Shifting From a Single-Page to a Multi-Page Mindset
Because of the work that I've done historically with Angular.js (and modern Angular), it's hard to think about a different JavaScript framework and not think in terms of a Single-Page Application (SPA). But, Alpine.js isn't a SPA framework—it's a way to enhance HTML pages that are rendered on the ColdFusion server.
The first few examples in this post are a good exploration of how the Alpine.js scope mechanics work. But, I think the last example, in which ColdFusion renders the list of users on the server, is really how Alpine.js is intended to be used. Creating an item-based controller, instead of a list-based controller, keeps the code server-oriented; and works to make the DOM the source of truth.
This is a hard transition for me to make. And, I'll be transparent in saying that I'm not quite sure where the balance is to be struck. I'm still very much learning and creating my mental model.
Want to use code from this post? Check out the license.
Reader Comments
I recently started using HTMX with coldfusion. It's so good. It's what I always wanted from cfdiv only better.
@Chris,
HTMX is definitely my next exploration. It seems that HTMX is often used alongside Alpine.js as well; so, this is all sounding very good. Maybe I'll start looking into it this weekend.
Any hot tips for early pitfalls in learning HTMX?
I can't speak to Chris's experiences, but I've started using htmx recently myself and I really like it. It has the sort of intuitive use of ColdFusion.
For my part, I typically have a URL to the parent page and then an "hx-" attribute pointing to a URL with just the part of the page I want - but that is just as a back-up in case JavaScript isn't working.
I am curious on your thoughts on making the DOM the source of truth. I played with Alpine a bit and I like that, though it seemed a bit harder than expected to keep the functionality in JavaScript files.
Whereas Stimulus.js keeps DOM as the source of truth but makes it easier to keep functionality in separate JavaScript files (though the syntax isn't as pleasingly concise as in Alpine).
What is your take on DOM-as-source of truth and the location of the functionality aspects of JavaScript?
@Steve,
Coming off of 13-years of Angular(js), where ColdFusion was just an API in 95% of use-cases, and Angular did all of the JSON -> DOM generation, it's really really hard (for me) to shift into a DOM-as-truth mindset. I keep wanting to make calls back into the JavaScript context to "change data" and then have that data reflected.
Alpine.js makes this a bit easier, since it has some of the reactive functionality that Angular sort of had (with its
x-text
andx-for
attributes, for example). Stimulus (which is what I'm currently using on the blog), has much less of that; and really forces you to put state in the DOM. But, both of them work usingMutationObserver
; they just draw the hard-line in different places.With Stimulus.js, the syntax is just wearing me down. The extreme verbosity means that there's no confusion as to what is going to get called and where the inputs are coming from. But, it's just too much. I fear that I will start to sacrifice my naming conventions in order to minimize the size of attributes. But, that feels like solving the wrong problem.
I really need to start playing with HTMX as I feel like right now I'm trying to "work my way forward" from the UI to the server; and, I need to start "working my way backward" from the server to the UI. I think that will help me figure out where the line need to be drawn (for my level of comfort).
@Ben,
I definitely hear you about how verbose Stimulus.js is. We'll see if that bothers me over time. I have found that I have the controller create the "data-action" attribute itself if that is obvious from the nature of the controller, but the long attribute names can be a bit much.
I'm hoping that will lend itself to encourage code reuse in a way that Alpine.js does not seem to do.
My bigger issue with Alpine was that it was bit of a pain to have it play nicely with CSP. which is a non-issue with Stimulus.js.
Also, I've really been a fan of DOM-as-truth for a while, so I like that aspect.
I'd really love something that worked like Stimulus, but had some Alpine-like shortcuts, but I guess you can't have everything. ;-)
@Steve,
Yeah, CSP is a big question-mark for me at the moment. My blog has a strict CSP header; so, if I try to use Alpine.js, that's going to be a huge ergonomics limitation. As you're saying, I have Turbo, which is very CSP compatible because there are no expression in the DOM - just property labels.
I haven't explored the CSP stuff. I have downloaded the extension for it, but haven't used it yet. Will have to make that my next exploration to see if it becomes a deal-breaker.
On a CSP-related note, as a fun experiment, I tried to fork-lift the Angular.js 1.x parser and use it to create a custom evaluator in the Alpine.js CSP build:
www.bennadel.com/blog/4612-using-the-angular-js-parser-to-comply-with-csp-in-alpine-js-3-13-5.htm
It's just an experiment, and isn't feature-completed; but, I am able to enable Alpine.js to use most of the normal expression evaluation while still staying CSP compliant at the expense of like 30kb more code 😆
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →