Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Emily Meyer
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Emily Meyer

Using ColdFusion Custom Tags And Modules In CFWheels

By
Published in Comments (4)

I'm a huge fan of ColdFusion custom tags. Both for the encapsulation of complex logic such as with HTML email generation and for the context isolation provided by the CFModule tag. Custom tags can also be used to create simple abstractions for rich user interface (UI) controls. Just look at the Flux component library for Livewire. While Flux is built on PHP Blade templates, the concepts are the same. In the CFWheels ColdFusion framework, UI components are abstracted behind global functions; but, I think it would be worth exploring the use of ColdFusion custom tags as a UI abstraction in Wheels. But first, we have to figure out how to bake the "Wheels Magic" into the ColdFusion custom tag mechanics.

At its core, Wheels augments the execution context of all Models, Views, and Controllers (MVC pattern) by:

  1. Implicitly managing the control flow for you.

  2. Including / injecting a bunch of functions into every controlled context.

The "problem" with invoking a ColdFusion custom tag or module in Wheels is that Wheels doesn't have any hooks into that control flow. As such, it has no way to inject the global functions into the custom tag execution space. If we want to use the Wheels functions inside a custom tag, we have to give it a helping hand.

I don't believe there's any established pattern in the CFWheels community for doing this, so I'm just making this up as I go. My first attempt at this proof of concept (POC) is to take whatever functionality is available in the current Controller and append it to the custom tag context.

In Wheels, every controller is intended to extend a root Controller.cfc, which in turn, extends the internal wheels.Controller component, which in turn, includes all of your application's view helpers, and extends the internal wheels.Global component, which in turn, CFInclude's a host of Wheels global functions as well as your application's global function utilities.

There's a lot going on; and, it's spread across a number of different places. So, the path of least resistance here is to just take all of the functionality that gets implied within the Controller context and add it to the ColdFusion custom tag context.

To do this, I'm adding some logic to the application's root Controller.cfc that does two things:

  1. It caches the current Controller instance in the request scope.

  2. It defines a request-scoped function - .helpers() - for injecting said Controller functions into the variables scope of the calling context.

Here's a truncated version of my root Controller.cfc:

component extends="wheels.Controller" {

	// Include controller-wide utility functions.
	include "./functions/aaaa.cfm";
	include "./functions/bbbb.cfm";
	include "./functions/cccc.cfm";

	// --------------------------------------------------------------------- //
	// --------------------------------------------------------------------- //

	/**
	* Since CFWheels doesn't have a hook into the ColdFusion custom tag life-cycle, we're
	* going to piggy-back on the Controller inheritance chain in order to cache the
	* controller instance as the "bag of methods" that we'll explicitly "mix" into the
	* custom tag context.
	*/
	request.sourceControllerInstance = this;

	/**
	* Create a request-level function to be invoked IN THE CUSTOM TAG PAGE CONTEXT that
	* will append the above controller instance public methods into the page context of
	* the calling custom tag. When this method is invoked, it will be invoked AS IF it
	* were operating in the custom tag context. This is how ColdFusion works.
	*/
	request.helpers = $copyControllerFunctionsIntoPageContext;

	/**
	* I copy the public methods from the cached controller instance into current page
	* context (which is assumed to be a custom tag, but which would work for context that
	* has a variables scope).
	*/
	private void function $copyControllerFunctionsIntoPageContext() {

		// If we've already injected into this context, no need to process again.
		// --
		// Note: this is meaningful because ColdFusion custom tag templates can be
		// executed any number of times for a given tag context and we don't need to be
		// repeating this work over-and-over.
		if ( variables.keyExists( "sourceControllerInstance" ) ) {

			return;

		}

		var source
			= variables.sourceControllerInstance
				= request.sourceControllerInstance
		;

		for ( var key in source ) {

			var value = source[ key ];

			// Mix-in functions that don't conflict with the custom tag page context.
			if ( isCustomFunction( value ) && ! structKeyExists( variables, key ) ) {

				variables[ key ] = value;

			}

		}

	}

}

This code uses a subtle but important feature of ColdFusion: when you execute a function that isn't bound to a ColdFusion component, ColdFusion will execute said function in the context of the calling template. Which means, when request.helpers() is invoked, the variables scope referenced within the helpers() method is not the Controller.cfc scope — it's the page context associated with the calling template.

This is why we have to cache the current controller instance in the request scope as well - when we invoke helpers() from within our custom tag, it will no longer have any contextual information about the context in which it was defined, on the context in which it is executed.

Now, within our ColdFusion custom tag, all we have to do is invoke request.helpers() and we will wire-up all of the magic that is the CFWheels framework. To demonstrate, I have a view template that does nothing but invoke a custom tag:

<cf_MyTag>

This custom tag creates a strong execution boundary around the MyTag.cfm template. But, within that boundary, we will inject all of the Wheels goodness:

<cfscript>

	request.helpers();

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// Global function provided by app helpers.
	service( "ErrorLogger" )
		.info( "Ben is testing stuff!" )
	;

	// Global function provided by Wheels (converts camel-case to hyphens).
	for ( token in hyphenize( "onceMoreIntoTheFray" ).listToArray( "-" ) ) {

		// Global function provided by Wheels.
		echo( simpleFormat( token ) );

	}

</cfscript>

The first thing we do inside our custom tag is invoke request.helpers(). This will take all of the Functions available in the current controller and append them to the variables scope of the custom tag. This gives the custom tag internals access to everything. The service() method for creating services, the model() method for creating database persistence, and every other global function (ex hyphenize() and simpleFormat()) provided by the CFWheels framework.

Which is why, when we execute the view template that invokes MyTag.cfm as a ColdFusion custom tag, we get the following output:

once
more
into
the
fray

As you can see, once we called request.helpers() at the top of the ColdFusion custom tag, we subsequently had access to all of the CFWheels functions. This isn't doing anything revolutionary; but, it does open the door to using custom tags in a Wheels application that follows the "Wheels Way".

Want to use code from this post? Check out the license.

Reader Comments

279 Comments

Cleaver! And elegant. I've just been making ample use of CF's saveContentall over the place.

Naming things is so hard. I'm wondering how you settled on helpers(). Since there's already a concept of "helpers" in CFWheels, this name confused me initially. I'm thinking more like like cfw_magic() lol

Seriously — good stuff! Whenever I see you delving into CFWheels, I get excited. I wrote most of my site 13 years ago and have been in maintenance mode for so long, that it's been a while since I've thought about the fundamental mechanics of the framework. And I love getting your fresh take.

16,058 Comments

@Chris,

Ha, yeah, I settled on helpers because it was kind of a thing in Wheels already - I thought it might reduce confusion 😜 but yeah, like I said I'm just making this up as a I go. I agree that something more explicitly "wheelsy" would be good, like injectWheelsFunctions(). I was trying to balance clarity and brevity.

16,058 Comments

re: CFSaveContent, that's part of what I'm trying to get around. Ideally, there are some UI elements that I'd like to put in a custom tag instead of a helper function, because I think it just lends well to that form-factor. But, I'm still very much exploring.

279 Comments

@Ben Nadel,

For sure! Striking that balance between brevity and clarity is always a struggle. Personally, I tend to err on the side of clarity over brevity. But this is definitely a "choose your own adventure" sort of endeavor.

My helpers really needs some organization and general clean-up. They've gotten a bit out of hand over the years, so I definitely see the value in going the custom tag route. Keep this up and you may force me to refactor! Ha!

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