Collocating My ColdFusion, CSS, And JavaScript Files
When building a ColdFusion multi-page application (MPA), I've never been satisfied with how files are organized. Client-side files (CSS and JS) that are tightly coupled to server-side files (CFML) are often located in completely different parts of the application's folder structure. This adds friction to the maintenance of the ColdFusion application. In an effort to experiment with a more cohesive file strategy, I want to try putting CSS and JavaScript files right next to their CFML counterparts.
What this means is that each ColdFusion interface may have four different files working together to create the user experience (UX). A login form, for example, may have the following files:
form.cfm
- the "controller" template.form.view.cfm
- the "view" template.form.view.less
- the CSS file for the view.form.view.js
- the JavaScript file for the view.
This may feel verbose; but, it's really no different than the number of files one might normally have; only, the files are right next to each other rather than spread across various server and client focused folders.
If you're coming from the Angular world, this should feel very familiar as Angular components use this same multi-file collocation technique. For example, a form component might entail the following files:
form.component.ts
- the "controller" code (which, in Angular, also acts as the JavaScript file for the view).form.component.html
- the "view" template.form.component.less
- the CSS file for the view.
Essentially, I'm trying to get my ColdFusion multi-page application (MPA) file organization to look more like my Angular single-page application (SPA) file organization. The collocation of files has proven to be very powerful in the SPA world; and, I have no doubt that it will be equally powerful in the MPA world.
Of course, in the MPA world you don't have all of the same developer ergonomics that you have in the SPA world. Which means, I'm going to be manually minding the gap in this exploration. Specifically, instead of getting my emulated CSS scoping from the JavaScript compiler, I'm going to be manually adding some random attributes to my CFML markup that my Less CSS can hook into.
I don't love the idea of doing this manually; but, I also don't want to let the perfect become the enemy of the good. I'd rather explore the space with some manual labor than waste time trying to figure out how to programmatically add CSS scoping.
To this end, I created a random slug generator utility that allows me to randomly generate a 6-character token. This token will then act as a namespace in two ways:
It will be added as an attribute to all HTML elements that also need to have a scoped CSS class.
It will be added to the global JavaScript scope as an object in which to define Alpine.js controllers.
Take the slug, 784nz3
, as an example. In my CFML file, I'll use this to both define x-data
attributes and to scope CSS classes:
<form
x-data="m784nz3.FormController"
m-784nz3
class="form">
<button type="submit" m-784nz3 class="submit">
Submit Form
</button>
</form>
Note: I'm prefixing the slug with
m
to ensure that it always starts with a letter (in order to ensure a proper JavaScript variable name). I probably should just make that a constraint of the slug generator.
My collocated Less CSS file will then define a block for this attribute, [m-784nz3]
; and all CSS classes will be scoped under it using the &.
operator:
[m-784nz3] {
&.form {
// ... scoped form styles ...
}
&.submit {
// ... scoped button styles ...
}
}
This gets compiled down into CSS that looks like [m-784nz3].form
and [m-784nz3].submit
, ensuring that these CSS classes don't affect any other user interface elements accidentally.
My collocated JavaScript file then performs two jobs: it includes the .less
file (a feature of the Parcel JS bundler that I'm using); and, it defines the Apline.js component(s) for the view:
// Import and emit the collocated Less CSS file.
import "./form.view.less";
// Make Alpine JS component available on global scope under unique slug.
window.m784nz3 = {
FormController
};
function FormController() {
if ( window.location.hash ) {
document.querySelector( "input[ name = 'redirectTo' ]" )
.value += window.location.hash
;
}
}
The unique and random slug, 784nz3
, is now effectively scoping both the CSS and the JavaScript for the collocated set of files that holistically represent a view within my ColdFusion application.
I then need a root JavaScript file to pull the whole system together into a compiled, bundled set of resources. This is what my main.js
file looks like—it globs (**/*
) over the folder structure looking for all *.view.js
files:
// Import vendor modules.
import Alpine from "alpinejs";
// Import app modules.
import "../cfml-main/views/**/*.view.js";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
window.Alpine = Alpine;
Alpine.start();
For every .view.js
file that gets included by the above import
, the relevant .less
is also included (since it gets included as a module by each view-specific JavaScript file). This ultimately traverses the whole ColdFusion user-facing folder structure and compiles all of the Less CSS and JavaScript files into a .css
and .js
file, respectively, that I can then include in my layout rendering.
Using this approach, if I have to modify the look and feel or behavior of the form, all of the relevant files are right there—there's no confusion about whether or not I'm looking in the right place; or, if the relevant files already exist. And, if I need to delete the form from the application, I can easily delete all four collocated files and have no concern about any crufty, left-over CSS or JavaScript files being left accidentally in the compiled code.
This makes the code easy to find and easy to delete. Which are two foundational principles underpinning my theories on clean code architecture.
Again, I don't love the idea of having to manually generate tokens and apply them to my CFML markup. But, I'm not going to let a little extra work block this experiment—an experiment that has already made my development life easier.
You can see this in action in my Feature Flags playground app. If you look at the "feature targeting" view, for example, you'll see four files that work together to create the user experience.
So far, this is working well in my feature flags app. And, if this continues to feel right, I'll try to retrofit this approach into my blog as well (where I do much of my "production ready" experimentation). And, if anyone has any suggestions, I'm all ears. It would seem that this is a "solved" problem; but, it also seems that most people only have solid solutions for SPA architectures; and that MPA architectures are still a little "wild West" in terms of file organization and scoping.
Want to use code from this post? Check out the license.
Reader Comments
This is either going to make people laugh or tick them off. Either way, I'm going for it.
Ahem ...
Hey! Aren't those called "fuses"?
@JC,
Ha ha, I mean, when you boil it down, all apps have to render views. Each framework is going to call it differently. Way way way back when I was learning about Fusebox, I'm sure that we even had a concept of JavaScript / Less CSS build systems. So, yes, in a sense I'm trying to figure out how to take the simplicity of that kind of an app but modernize it a bit for ease of full-stack development.
Hi Ben
I think in FW1, it uses the following way of organising things, by default:
I can never make up my mind whether we should categorise by MVC or by section? Theoretically, I think you can rewire stuff in FW1, in the
application.cfc
, using:So you could probably group pages by section like:
But, I'm not entirely sure what the actual advantage would be?
I would have thought this is down to personal preference, rather than it providing any functional/practical advantage?
But, I could well have missed something, here? 🤔
@Charles,
To be upfront, I'm still trying to find a pattern that feels great. So, I won't pretend to have all the answers. That said, I'm starting to think more about applications in terms of a "core" and then various "clients".
The "core" is where all the fundamentally important logic that makes the application what it is - all the models and data access and email sending and all that jazz.
The "clients" is then where all the routing and view-rendering logic would go. This would be where put my user-facing CFML controllers, JS, and CSS files.
But, I'm still trying to figure out where everything goes. Take for example something like an Authentication Service. My traditional notion would be that Authentication goes in the "core", period; and, maybe some of it does. But, if you think about the implementation of the authentication mechanism, maybe it's cookie-based in one area of the application and HTTP header-based in another area of the application. In that case, it feels a bit strange to just stuff it all in the application "core".
Probably, what I would need to do is put cookie-based mechanics into one area of the "client" app; and then header-based mechanics into another area of the "client" app; and then, perhaps both of them can reach into the application "core" for certain implementation details like data-access.
But, like I said, I'm still trying to feel-out where things go.
Your coding is far more sophisticated than mine but I also spent years trying to figure out what would simplify my development process where I could drag and drop a subset of files from one app to another. To make it work all I had to do was add the tables for the database, a line to instantiate each added component(s) in the application file and add the new navigation to the navbar.
My solution which I think I've been using for a decade is FW/1 with subsystems. Each subsystem is a section of the site. I've tweaked it a bit by adding css and JS pages unique to each subsystem and each action so if for example a css file exists in the subsystem css folder, it will load it. If a defaultJS.cfm file exists in the same folder as the default.cfm file it loads it... Simple but it works. The only issue is when I change a framework like bootstrap or fontawesome but I'm working on that. :)
@William,
I'm all for whatever is the most simple solution that works. I feel like I've spent the last 10 years swinging to the "robust but complicated" end of the spectrum. And now, I've spent the last year or two swinging back the other way towards "effective enough but simple."
Using each FW/1 subsystem as a section on the site is an interesting approach - I haven't thought of that before. Historically, I've been using subsystems to define macro areas (like areas that have a totally different authentication system). But, it was just an arbitrary decision that my team made that we then stuck with while maintaining an application. The subsystem per nav link is interesting because it does definitely give you ways to split up the CSS and JavaScript.
Your advice to prove the hexagonal architecture:
https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)
@Roberto,
I'm not versed enough in the academia of application architecture to speak to the hexagonal pattern specifically. In the Wiki article, they also reference the onion architecture as well as Robert Martin's "clean code" architecture. But, all in all, I think they're all aimed at reducing tight coupling by trying to hide implementation details and communicate through boundaries. I think.
As I'm building out my applications, I do try to keep loose coupling in mind always. I think Inversion of Control (IoC) is one of the most powerful patterns that we have at our disposal. And, my
Injector.cfc
makes heavy use of this.With that said, most of the reading that I've done about architecture is more back-end focused; which leaves client-side file organization a bit of a question-mark. My post here is more specifically trying to deal with the client-side file organization, by attempting to better align it with the back-end file organization (with a focus on loose coupling).
@Ben Nadel,
It works for me. It may not be everybody's cup of tea.
To handle access control for subsystems I found a simple but effective cfc written by someone a very long time ago. I would credit them but I can't find their information. If that person reads this, let me know and I will give you credit. : )
At the time I was looking for ways to manage security on a granular level for FW1 with subsystems. To use it for my purpose I modified it to handle multiple roles per rule. I also migrated the rules (see code) to the database for some projects.
You'll find it here in my repo: https://github.com/cheebu/roleManager
So you can limit access to a subsystem or even a particular template within that subsystem. You can also choose to hide or display menu items with a simple cfif isUserInRole function which would be pretty easy to write, but hit me up by email if you want me to send you the component for reference.
Side note: In case you are wondering why I created my own "isUserInRole" function... I stopped using cflogin because it was buggy in CF3.x/CF4.5. I still don't like the not so secure client cookies it creates today without wrestling with TomCat configurations. So I created my own login, role definition and management components way back. I gave up on most of the CF gimmick code years ago... cflogin, cfform, cfchart etc mostly because it was buggy and out-of-date by time it was launched. Anyway, that's for another post.
All said, there is no single/ approach to making your code easy to manage or future proof but it's fun to try. 😊
@William,
We actually use something very similar at work. But, I think the mistake we made was not encapsulating it within a CFC like you have. Basically, in our FW/1 application we queue-up a subsystem controller like:
In our case, the
common:
controller would attach the user to the current request (ie, looking at the session cookies and cross-checking with the database). Then, the subsystem-specific controller,:security.authorize
, would basically do what yourcheckUser()
method is doing. We even have a "whitelist" struct as well for allowing unauthorized access to certain FW/1 actions/items.The part about our approach that never sat well with me is the division between the two different controllers. I kind of just wish we had some security logic at the top of each subsystem default controller.
In general, that's one thing that I always felt was missing in FW/1 - subsystem-wide interceptors. Though, maybe those exist now in the latest releases (our version of FW/1 was very old). I know that within a controller, you can do all the
before()
andbeforeEach()
intercepting. But, I always felt like there should be a mechanism for subsystem-wide interception. A place to put subsystem-wide error handling and security checks.@Ben Nadel,
I use the gold version 4.x of FW/1. I think you'll find it surprisingly easy to update if you ever decide to make the change.
I do the same (see below) - calling it at the end of setupRequest(). Putting it in each before() of each subsystem would require too much maintenance and easily forgotten when a new subsystem is created or moved from one project to another.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →