Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Jeremy Mount
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Jeremy Mount

RAGE: Controller And Action ColdFusion Components

By
Published in Comments (5)

Note: This is a work in progress. It has been field tested to some degree, but not finalized.

As I wrote before, I have been fooling around with a way to overcome the inadequacies of the traditional action variable and what I have come up with I have coined Relative Action Group Execution or RAGE. The RAGE action variable is a string of item and key pairs (a single pair is an action group) chained together. For example, you might see something like:

attorney:14.bio:3.edit

This would define the page's action as being the editing of the bio(graphy) whose ID is 3 which belongs to the attorney whose ID is 14.

Now, above, I say "belongs to", but that is just a mental construct that makes me comfortable. From a programmatic stand point, the RAGE action variable just defines the actions that must be chained together to reach the page's full action potential. The relationship of actions is purely Parent-Child, but can be thought of "belongs to" or "having" depending on the situation. For instance, you might have the RAGE action:

attorney.list

This would indicate that we are listing attorneys. In this case, there is no sense of belongs to, but rather, just a sense of the chaining of actions or modules.

RAGE Action Group Parsing

The RAGE Controller parses the action strings (examples above) into a quasi-linked list / array stack of RAGE Actions. Both of these are represented by ColdFusion components (see below, RageController.cfc, RageAction.cfc). Each action consists primarily of an Item and a Key. Taking the first example above, attorney:14.bio:3.edit, this would give us the group stack:

Group 1

  • ITEM: attorney
  • KEY: 14

Group 2

  • ITEM: bio
  • KEY: edit

The groups are contained in an array that has a one-way sense of linking; each group knows about its parent group. I have found that knowing about your child is not very important.

This group array / stack gets parsed once for each page request. It can be parsed again arbitrarily using the ParseAction() method which will effectively clear the stack and re-initialize it.

Relative Navigation

As I touched on in an earlier post, other than the ease with which this variable can be passed around (it's just ONE variable), the thing that I LOVE about it the most is that it makes jumping around in the architecture of a site very easy and abstract. Its strength is jumping up the chain in an immediate relative manner. Each RAGE action group ColdFusion component instance has several navigational methods:

ToParent( [Action1 [, Action2 ]] )

The ToParent() method is designed to move back up the action group chain. It can however be used to move across and down (purely as a short hand notation). When sent with no arguments, this method goes up one group and returns the URL for the parent group. If an item is passed as the first argument, then the method goes up to the parent and then back down to the requested item. If the a key is passed as the first argument and the item as the second, then the method goes up to the parent, sets the key value in the parent, and then goes down to the requested item.

ToSibilng( Action1 [, Action2 ] )

The ToSibling() method is designed to move to another action that is working on the same parent group. Think about going from an EDIT screen to a VIEW screen. Both of these actions are being performed on the same parent group. If the first argument is an item (this is expected), then the method basically swaps out the current item with the requested item and returns the URL. If the first argument is a key and the second argument is an item, it does the same thing except it also sets the parent key (I am not sure why this would ever be done, but I built it in anyway for ease).

ToChild( Action1 [, Action2 ] )

The ToChild() method is designed to move to a requested child group. If the first argument sent in is an item (this is expected) then the method appends the requested item to the current group and returns the URL. If the first argument is a key and the second argument is an item, then the method sets the current groups key before appending the requested item and returning the URL.

Navigational Examples

That all sounds complicated, but it is pretty easy to wrap your head around once you get going. The real key to all of this is that you never really have to know where you are in the system; you only have to know who your parent is, who you (action group) are, and where you want to go relative to that. Let's look at some examples based on where you are and where you want to get.

At: attorney:14.view
To: attorney:14.edit

Preferred method: RAGE.ToSibling( "edit" )
Other method: RAGE.ToParent( 14, "edit" )

At: attorney:14.edit
To: attorney.list

Preferred method: RAGE.ToParent( "list" )
Other method: RAGE.ToSibling( 0, "list" )

At: attorney.list
To: attorney:14.view

Preferred method: RAGE.ToChild( 14, "view" )
Other method: RAGE.ToSibling( 14, "view" )

At: attorney:14.edit
To: attorney:14.api.delete

Preferred method: RAGE.ToSibling( "api.delete" )
Other method: RAGE.ToParent( 14, "api.delete" )

At: attorney:14.edit
To: attorney

Preferred method: RAGE.ToParent()

At: attorney:14.bio:3.edit
To: attorney:14.edit

Preferred method: RAGE.Parent.ToParent( "edit" )

As you can see, it is very easy to jump around in a relative manner. The strength is one parent-child chain jump. In the last example, however, you can see that multi-group jumping can still be done relatively by accessing the Parent object of the current group (and then performing relative actions on it).

The whole point here, that you can hopefully see, is that you never have to be aware of WHERE you are in the system.

There is a bit of other functionality that is cool, like being able to create the RAGE string from a base url so that you do not need to know the root URL (similar functionality is seen in FuseBox). But anyway, here is the code for the components.

RageController.cfc ColdFusion Component

<cfcomponent
	displayname="RageController"
	output="false"
	hint="Handles the RAGE action flow and value parsing.">


	<!--- Run the pseudo constructor. --->
	<cfscript>

		// Set the default public data. Currently, I am making much of the information
		// public as I want to limit the number of method calls made on the object.
		// This needs to run for every page and I am not worried about security.

		// This is the URL of the requested action.
		THIS.Self = "";

		// This is the root url of the application (without the query string but
		// including the controller file (usually index.cfm).
		THIS.SiteUrl = "";

		// Group pointers for various points in the action chain. These values
		// will change as actions are pushed and popped off of the stack. The
		// first and last group pointers refer to the top-most and bottom-most
		// groups in the RAGE action stack.
		THIS.FirstGroup = "";
		THIS.LastGroup = "";

		// The current group is a reference to the action group to which we are
		// currently pointing.
		THIS.CurrentGroup = "";

		// This is parent group of the current group.
		THIS.ParentGroup = "";

		// This is the stack that keeps the linked actions.
		THIS.Groups = ArrayNew( 1 );

		// This is the action index (of the stack) at which the current group can
		// be found.
		THIS.GroupIndex = 0;

		// This was the original RAGE action that was parsed into the stack.
		THIS.OriginalAction = "";

	</cfscript>


	<cffunction name="Init" access="public" returntype="any" output="false"
		hint="Returns an initialized RAGE controller.">

		<!--- Define arguments. --->
		<cfargument name="SiteUrl" type="string" required="true" />
		<cfargument name="Action" type="string" required="false" default="" />

		<cfscript>

			// Store the site url.
			THIS.SiteUrl = ARGUMENTS.SiteUrl;

			// Store the self.
			THIS.Self = (THIS.SiteUrl & "?rage=" & ARGUMENTS.Action );

			// Parse the given action.
			THIS.ParseAction( ARGUMENTS.Action );

			// Return This reference.
			return( THIS );

		</cfscript>
	</cffunction>


	<cffunction name="ParseAction" access="public" returntype="numeric" output="false"
		hint="Parses the action into appropriate groups and returns the number of groups that were added to the stack.">

		<!--- Define arguments. --->
		<cfargument name="Action" type="string" required="true" />

		<cfscript>

			// Define the local scope.
			var LOCAL = StructNew();

			// Store the action as the original action.
			THIS.OriginalAction = ARGUMENTS.Action;

			// Add some buffer space to the delimiters in the RAGE string. This will help
			// us when we are looping through the items (so that there will always be the
			// correct number of values in each group.
			ARGUMENTS.Action = ARGUMENTS.Action.ReplaceAll( "([.:]{1})", " $1 " );

			// Split the string into action groups.
			LOCAL.Groups = ARGUMENTS.Action.Split(
				"\.",
				JavaCast( "int", 2 )
				);

			// Create a string buffer to hold the HREF as we build the aciton groups.
			LOCAL.Href = CreateObject( "java", "java.lang.StringBuffer" ).Init(
				THIS.SiteUrl & "?rage="
				);

			// Create a default RAGE action group. This might seem like a silly thing to
			// do, but by having a default, empty group, it makes creating the parent
			// group pointers easy to do with zero logic.
			LOCAL.RageAction = "";


			// Loop over the groups.
			for (
				LOCAL.GroupIndex = 1 ;
				LOCAL.GroupIndex LTE ArrayLen( LOCAL.Groups ) ;
				LOCAL.GroupIndex = (LOCAL.GroupIndex + 1)
				){

				// Split the current group up into it's two values. Append the value
				// delimiter to the group value. This might create more than TWO values,
				// but it will insure that we have AT LEAST two values.
				LOCAL.Group = ToString(LOCAL.Groups[ LOCAL.GroupIndex ] & " : ").Split(
					":",
					JavaCast( "int", 2 )
					);


				// Check to see if we need to add the group delimiter. This will only be
				// required after the first loop iteration (where we will have a parent
				// group off of which this one builds).
				if (LOCAL.GroupIndex GT 1){

					// Add the action group delimiter.
					LOCAL.Href.Append( "." );

				}


				// Create a new RAGE action group with these group values. The Item is
				// a string, but the key is always numeric. Force numeric using VAL()
				// rather than validating as there is no point to validate here. Also,
				// be sure to trim both values as we buffered to get appropriate value count.
				LOCAL.RageAction = CreateObject( "component", "RageAction" ).Init(
					Controller = THIS,
					Parent = LOCAL.RageAction,
					Item = LOCAL.Group[ 1 ].Trim(),
					Key = Val( LOCAL.Group[ 2 ].Trim() ),
					ItemHref = LOCAL.Href.Append( LOCAL.Group[ 1 ].Trim() ).ToString()
					);


				// Add this action to the RAGE queue.
				ArrayAppend( THIS.Groups, LOCAL.RageAction );


				// Add the key to the running href. This will only be used for the next
				// iteration in which case this key will be the parent's key. Only do
				// this if the key is non-zero.
				if (LOCAL.RageAction.Key){

					LOCAL.Href.Append( ":" & LOCAL.RageAction.Key );

				}

			}


			// Check to see if any groups were parsed. If there were, then set
			// more appropriate default values.
			if (ArrayLen( THIS.Groups )){

				// Point at the current group.
				THIS.GroupIndex = 1;

				// Set up the group pointers.
				THIS.FirstGroup = THIS.Groups[ 1 ];
				THIS.LastGroup = THIS.Groups[ ArrayLen( THIS.Groups ) ];
				THIS.CurrentGroup = THIS.FirstGroup;

				// Set up parent group pointers. Since we are initializing the
				// group stack, we are going to be pointing to the first group
				// and therefore will not have any parents.
				THIS.ParentGroup = "";

			}


			// Return the number of groups that were created. This can be considered a boolean
			// determining if any valid action groups were found.
			return( ArrayLen( THIS.Groups ) );

		</cfscript>
	</cffunction>


	<cffunction name="Pop" access="public" returntype="any" output="false"
		hint="Moves the current group pointer down my one in the stack and returns the current RAGE action group.">

		<cfscript>

			// Define the local scope.
			var LOCAL = StructNew();

			// Check to see if we have any groups left to "pop" off the stack. We never
			// want to throw an error during a pop, so if an invalid one is requested,
			// add a NULL-type action group to the stack.
			if (THIS.GroupIndex GTE ArrayLen( THIS.Groups )){

				// Create a null-type action group.
				LOCAL.NewGroup = CreateObject( "component", "RageAction" ).Init(
					Controller = THIS,
					Parent = THIS.LastGroup,
					Item = "{null}",
					Key = 0,
					ItemHref = (
						THIS.LastGroup.ItemHref & ":" &
						THIS.LastGroup.Key &
						"." & "null"
						)
					);


				// Add this group to the stack.
				ArrayAppend( THIS.Groups, LOCAL.NewGroup );

				// Update the last group to be the newly created group.
				THIS.LastGroup = LOCAL.NewGroup;

			}


			// Increment the group index.
			THIS.GroupIndex = (THIS.GroupIndex + 1);

			// Update the current group pointer.
			THIS.CurrentGroup = THIS.Groups[ THIS.GroupIndex ];

			// Update the parent group to be for the current group's parent.
			THIS.ParentGroup = THIS.CurrentGroup.Parent;


			// Return the current group.
			return( THIS.Groups[ THIS.GroupIndex ] );

		</cfscript>
	</cffunction>


	<cffunction name="SetHrefs" access="public" returntype="void" output="false"
		hint="This should only be called when a key value of a group has been manually updated. This updates all the item Hrefs.">

		<cfscript>

			// Define the local scope.
			var LOCAL = StructNew();

			// Create a string buffer to hold the HREF as we re-build for each action group.
			LOCAL.Href = CreateObject( "java", "java.lang.StringBuffer" ).Init(
				THIS.SiteUrl & "?rage="
				);

			// Loop over the groups.
			for (
				LOCAL.GroupIndex = 1 ;
				LOCAL.GroupIndex LTE ArrayLen( THIS.Groups ) ;
				LOCAL.GroupIndex = (LOCAL.GroupIndex + 1)
				){

				// Check to see if we need to add the group delimiter. This will only be
				// required after the first loop iteration (where we will have a parent
				// group off of which this one builds).
				if (LOCAL.GroupIndex GT 1){

					// Add the action group delimiter.
					LOCAL.Href.Append( "." );

				}

				// Set the item href of the current group.
				THIS.Groups[ LOCAL.GroupIndex ].ItemHref = LOCAL.Href.Append(
					THIS.Groups[ LOCAL.GroupIndex ].Item
					).ToString();

				// Add the key to the running href. This will only be used for the next
				// iteration in which case this key will be the parent's key. Only do
				// this if the key is non-zero.
				if (THIS.Groups[ LOCAL.GroupIndex ].Key){

					LOCAL.Href.Append( ":" & THIS.Groups[ LOCAL.GroupIndex ].Key );

				}

			}

			// Return out.
			return;

		</cfscript>
	</cffunction>


	<cffunction name="ToAction" access="public" returntype="string" output="false"
		hint="Appends the given RAGE action to the controller url (site url).">

		<!--- Define arguments. --->
		<cfargument name="Action" type="string" required="false" default="" />

		<!--- Return the url of the site href with the new action. --->
		<cfreturn (
			THIS.SiteUrl & "?rage=" & ARGUMENTS.Action
			) />
	</cffunction>



	<!--- ----------------------------------------------------------------------------------------------- ----

		These are short hands the forward the request onto the current group.

	----- ----------------------------------------------------------------------------------------------- --->


	<cffunction name="ToChild" access="public" returntype="string" output="false"
		hint="This provides a short hand for the related Current Group method.">

		<!--- Define arguments. --->
		<cfargument name="Action1" type="string" required="false" default="" />
		<cfargument name="Action2" type="string" required="false" default="" />

		<!--- Just pass this request straight to the current group. --->
		<cfreturn THIS.CurrentGroup.ToChild(
				ARGUMENTS.Action1,
				ARGUMENTS.Action2
				)
			/>
	</cffunction>


	<cffunction name="ToParent" access="public" returntype="string" output="false"
		hint="This provides a short hand for the related Current Group method.">

		<!--- Define arguments. --->
		<cfargument name="Action1" type="string" required="false" default="" />
		<cfargument name="Action2" type="string" required="false" default="" />

		<!--- Just pass this request straight to the current group. --->
		<cfreturn THIS.CurrentGroup.ToParent(
				ARGUMENTS.Action1,
				ARGUMENTS.Action2
				)
			/>
	</cffunction>


	<cffunction name="ToSibling" access="public" returntype="string" output="false"
		hint="This provides a short hand for the related Current Group method.">

		<!--- Define arguments. --->
		<cfargument name="Action1" type="string" required="false" default="" />
		<cfargument name="Action2" type="string" required="false" default="" />

		<!--- Just pass this request straight to the current group. --->
		<cfreturn THIS.CurrentGroup.ToSibling(
				ARGUMENTS.Action1,
				ARGUMENTS.Action2
				)
			/>
	</cffunction>

</cfcomponent>

RageAction.cfc ColdFusion component

<cfcomponent
	displayname="RageAction"
	output="false"
	hint="Represents an action group in the RAGE action stack.">


	<!--- Run the pseudo constructor. --->
	<cfscript>

		// Store a pointer to the RAGE controller.
		THIS.Controller = "";

		// Store pointers to the parent action group.
		THIS.Parent = "";

		// Set up default public properites.
		THIS.Item = "";
		THIS.Key = "";

		// Set the default HREF to this group item (without any key).
		THIS.ItemHref = "";

	</cfscript>


	<cffunction name="Init" access="package" returntype="any" output="false"
		hint="Returns an initialized Rage Action instance.">

		<!--- Define arguments. --->
		<cfargument name="Controller" type="any" required="true" />
		<cfargument name="Parent" type="any" required="true" />
		<cfargument name="Item" type="string" required="true" />
		<cfargument name="Key" type="numeric" required="false" default="0" />
		<cfargument name="ItemHref" type="string" required="false" default="" />

		<cfscript>

			// Store the controller reference.
			THIS.Controller = ARGUMENTS.Controller;

			// Store the parent action group pointer.
			THIS.Parent = ARGUMENTS.Parent;

			// Set up the properties to the item-key data.
			THIS.Item = ARGUMENTS.Item;
			THIS.Key = ARGUMENTS.Key;

			// Store the item href.
			THIS.ItemHref = ARGUMENTS.ItemHref;

			// Return This reference.
			return( THIS );

		</cfscript>
	</cffunction>


	<cffunction name="SetKey" access="public" returntype="void" output="false"
		hint="This sets the key value of the group and then asks that the RAGE controller recalculate all of the item HREFs.">

		<!--- Define arguments. --->
		<cfargument name="Key" type="numeric" required="true" />

		<!--- Store the given key. --->
		<cfset THIS.Key = ARGUMENTS.Key />

		<!--- Get the controller to the recalculate the hrefs. --->
		<cfset THIS.Controller.SetHrefs() />

		<!--- Return out. --->
		<cfreturn />
	</cffunction>


	<cffunction name="ToChild" access="public" returntype="string" output="false"
		hint="Goes to the child group by appending the requested RAGE action.">

		<!--- Define arguments. --->
		<cfargument name="Action1" type="string" required="false" default="" hint="This might be an item or a key. If it is numeric, it is a key, otherwise it is an item." />
		<cfargument name="Action2" type="string" required="false" default="" hint="If this is passed then the first argument is a key and this is an item." />

		<cfscript>

			// Define the local scope.
			var LOCAL = StructNew();

			// Figure out what the requested item and keys are. This depends on wether
			// or not the first argument is numeric (indicating that a key was passed).
			if (IsNumeric( ARGUMENTS.Action1 )){

				// Since the first argument is numeric, we are going to assume that
				// the first argument is the key and the second argument is the item.
				// Return the href to the new key and item.
				return(
					THIS.ItemHref & ":" &
					ARGUMENTS.Action1 & "." &
					ARGUMENTS.Action2
					);

			} else if (Len( ARGUMENTS.Action1 )) {

				// Since the first argument was a string value, we are going to assume
				// that only an item was passed in and that no key was available.
				// Return the href to the new item.
				return(
					THIS.ItemHref & "." &
					ARGUMENTS.Action1
					);

			} else {

				// No arguments have been passed in. Just send to SELF href.
				return( THIS.ItemHref );

			}

		</cfscript>
	</cffunction>


	<cffunction name="ToParent" access="public" returntype="string" output="false"
		hint="Goes to the parent group and appends the requested RAGE action.">

		<!--- Define arguments. --->
		<cfargument name="Action1" type="string" required="false" default="" hint="This might be an item or a key. If it is numeric, it is a key, otherwise it is an item." />
		<cfargument name="Action2" type="string" required="false" default="" hint="If this is passed then the first argument is a key and this is an item." />

		<!--- Just pass this request up to the parent. --->
		<cfreturn
			THIS.Parent.ToChild(
				ARGUMENTS.Action1,
				ARGUMENTS.Action2
				)
			/>
	</cffunction>


	<cffunction name="ToSibling" access="public" returntype="string" output="false"
		hint="Goes to the sibling group by appending the requested RAGE action.">

		<!--- Define arguments. --->
		<cfargument name="Action1" type="string" required="false" default="" hint="This might be an item or a key. If it is numeric, it is a key, otherwise it is an item." />
		<cfargument name="Action2" type="string" required="false" default="" hint="If this is passed then the first argument is a key and this is an item." />

		<cfscript>

			// Define the local scope.
			var LOCAL = StructNew();

			// Figure out what the requested item and keys are. This depends on wether
			// or not the first argument is numeric (indicating that a key was passed).
			if (IsNumeric( ARGUMENTS.Action1 )){

				// Since the first argument is numeric, we are going to assume that
				// the first argument is the key and the second argument is the item.
				// Pass this request up to the parent with the same arguments.
				return(
					THIS.Parent.ToChild(
						ARGUMENTS.Action1,
						ARGUMENTS.Action2
						)
					);

			} else if (Len( ARGUMENTS.Action1 )) {

				// Since the first argument was a string value, we are going to assume
				// that only an item was passed in and that no key was available.
				// Pass this up to the parent but send the parent's key as the given key.
				// We are doing this because we want to stay in the same "section" as
				// identified by both the parent's item and key.
				return(
					THIS.Parent.ToChild(
						THIS.Parent.Key,
						ARGUMENTS.Action1
						)
					);

			} else {

				// No arguments have been passed in. Just send to parent.
				return(
					THIS.Parent.ToChild()
					);

			}

		</cfscript>
	</cffunction>

</cfcomponent>

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

Reader Comments

36 Comments

Ben,

I am following your posts on RAGE. I can read the abstract above and get a decent idea of how this works. I think, along with others, a good object lesson, a quick prototype of this in action would really help me understand your concept in practice.

I know this is preliminary and you are working through the details so no pressure ;).

dw

15,902 Comments

@D.W.

I will be posting some more sample stuff on it soon. I figured one step at a time to keep it as simple as would be allowed. What I think will clear things up is you see how the Pop() method (not discussed yet) will interact with the page flow.

@D.S.

Good to know you have been using something like this for the past few years. That in an of itself is a great proof of the concept. I will be very interested to see what you think of my front controller (CFSwitch / Pop interaction).

36 Comments

Dan Switzer,

I easily understand the convention Ben uses on his blog. That is rather simplistic. Its more the interactions between the parents, children and the like that I would like to see practical examples of.

Some of this could get pretty hairy underneath and as organized as Ben appears to be, I would like to see his ideas in practice.

dw

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