Managing Lists Of IDs Using HTML FORM Posts In Lucee CFML 5.3.7.47
At InVision, I'm in the middle of building a custom feature for one of our clients. This feature lives outside of any SPA (Single-Page Application); and, uses "old school" form submission techniques to process the interactions. The techniques that I'm using are the same ones that I learned 2 decades ago. And they still work perfectly well! This is a testament to both the HTML specification and the way that ColdFusion simplifies the management of form submission data. And while this feels like "old technology" to me, it occurred to me that there might be little gems in here that newer developers don't know about. As such, I wanted to put together a quick demo on managing lists of IDs using HTML FORM posts in Lucee CFML 5.3.7.47.
Before we jump into the full demo, I think we need to build up some foundational understanding of the interplay between an HTML FORM post and the way in which ColdFusion exposes form submission data to the server-side processing. The most basic concept here is that multipart form-data is automatically parsed into the form
scope which we can then access as a set of key-value pairs.
By default, each name-value pair for inputs in the HTML FORM post shows up as a unique key-value pair in the form
scope. However, if you submit two inputs with the same name, the ColdFusion server will collapse those values down into a single list. To see this in action, let's try submitting an HTML FORM with several hidden inputs that all have the same name:
<cfoutput>
<form method="post" action="#cgi.script_name#">
<!---
These form fields will be submitted to the server as individual fields.
However, since they all have the SAME NAME, the ColdFusion server will
automatically collapse them down into a single, comma-delimited list.
--
NOTE: There is an Application.cfc setting to change the default behavior from
a LIST to an ARRAY (sameFormFieldsAsArray); but, I have not tried this.
--->
<input type="hidden" name="userIdList" value="1" />
<input type="hidden" name="userIdList" value="2" />
<input type="hidden" name="userIdList" value="3" />
<input type="hidden" name="userIdList" value="4" />
<input type="hidden" name="userIdList" value="5" />
<button type="submit">
Post User IDs
</button>
<a href="#cgi.script_name#">
Clear
</a>
</form>
<cfdump
label="FORM.userIdList"
var="#( form.userIdList ?: '' )#"
/>
</cfoutput>
As you can see, we have five hidden inputs all using the name, userIdList
, but each with a different value. And, when we submit this HTML FORM, we get the following output:
From the network activity, we can see that each of the hidden inputs was submitted as a separate form field in the multipart form-data payload. However, on the ColdFusion side, those form fields were all collapsed down into a single, comma-delimited list of the same name.
Apparently, there is an Application.cfc
(ColdFusion Framework Component) setting that changes the default "collapsing" behavior to use an Array instead of a List:
this.sameFormFieldsAsArray = true
ASIDE: There is a similar setting for URL parameters,
sameUrlFieldsAsArray
.
I have not used this because I work primarily with brownfield applications and changing this setting at this point in time would almost certainly break something in my ColdFusion applications. That said, we can use special naming conventions to change this behavior on a per-form basis. By taking the above example and suffixing the hidden input names with []
, it will signal to the ColdFusion server that the values should be coalesced into an Array, not a list:
<cfoutput>
<form method="post" action="#cgi.script_name#">
<!---
These form fields will be submitted to the server as individual fields.
However, since they all have the SAME NAME, the ColdFusion server will
automatically collapse them down into a single ARRAY (since the names are
all suffixed with "[]").
--->
<input type="hidden" name="userIdList[]" value="1" />
<input type="hidden" name="userIdList[]" value="2" />
<input type="hidden" name="userIdList[]" value="3" />
<input type="hidden" name="userIdList[]" value="4" />
<input type="hidden" name="userIdList[]" value="5" />
<button type="submit">
Post User IDs
</button>
<a href="#cgi.script_name#">
Clear
</a>
</form>
<cfdump
label="FORM.userIdList"
var="#( form.userIdList ?: [] )#"
/>
</cfoutput>
As you can see, this example is exactly the same. Only, instead of the form fields being named, userIdList
, they are named, userIdList[]
. And now, when we submit this HTML FORM, we get the following output:
From the network activity, we can see that each of the hidden inputs was submitted as a separate form field in the multipart form-data payload. However, on the ColdFusion side - just as in the first example - those form fields were all collapsed down into a single form
scope entry of the same name. Only, this time, that value is an array, not a list. Note that the []
suffix from the HTML FORM post is not part of the final form
entry.
The next HTML FORM behavior that we need to understand for this demo is that Submit Buttons can provide form data of their own. In their most basic implementation, a submit button just triggers the post-back of the HTML FORM. However, if you defined name
and value
attributes on a submit button, that name/value pair will be submitted as an entry in the form post. But - and this is a critical point - a submit button's name/value pair will only be included if the user clicked on that submit button.
What this means is that we can include multiple submit buttons in a single form; and then, use the name/value pairs to determine which action the user actually took (when submitting the form):
<cfoutput>
<form method="post" action="#cgi.script_name#">
<!---
When you design an HTML FORM, each submit button can have its own name-value
pair. And, when you use the submit button to submit the form, the button's
name-value pair is submitted along with the form data. As such, you can then
use that name-value pair to determine which action the user took.
--->
<button type="submit" name="action" value="DoThis">
Do This
</button>
<button type="submit" name="action" value="DoThat">
Do That
</button>
<button type="submit" name="action" value="DoOther">
Do Other
</button>
<a href="#cgi.script_name#">
Clear
</a>
</form>
<cfdump
label="FORM.action"
var="#( form.action ?: '' )#"
/>
</cfoutput>
As you can see, we have three different submit buttons each with the name, action
, but a different value
attribute. Now, when we click on each one of the buttons, we get a different form.action
value on the ColdFusion side of the workflow:
As you can see, with each HTML FORM submission, our form.action
entry has a different value.
Everything we've seen so far is "old school". This is how ColdFusion has worked for a really long time. And, this is how the HTML form specification has worked since for as long as I can remember. But, using these "old school" techniques, we can easily manage data.
To bring this all together, let's create a ColdFusion page that allows us to add and delete from a list of user IDs. You can imagine that this page would allow a customer to review and curate a list of data before some final processing step. Things of note in the following code:
We're using hidden inputs (all with the same name) to include the existing user IDs in each post-back.
We're using special
[]
suffix notation to collapse the like-named values into an Array.We're using submit buttons (all with the same name) to remove targeted user IDs from the list.
We're wrapping the whole page in a single HTML FORM tag so that all the data is posted back for each action.
Note that for the sake of simplicity, we're not inspecting the data or validating it in any way. Nor are we worrying about duplicate values.
<cfscript>
// Setup our default form field values.
param name="form.newUserID" type="string" value="";
param name="form.removeUserID" type="string" value="";
param name="form.userIDs" type="array" default=[];
// If a new ID was provided, add it to the collection!
// --
// NOTE: For the sake of simplicity, we are not going to do any special handling of
// commas in the value. We're just assuming that this is a controlled environment
// with valid inputs.
if ( form.newUserID.len() ) {
form.userIDs.append( form.newUserID );
}
// If a remove ID was provided, remove it from the collection!
if ( form.removeUserID.len() ) {
form.userIDs.delete( form.removeUserID );
}
</cfscript>
<cfoutput>
<form method="post" action="#cgi.script_name#">
<!--- NEW userID related fields. --->
<p>
<input
type="text"
name="newUserID"
value=""
placeholder="New user ID..."
autofocus
/>
<button type="submit">
Add User ID
</button>
</p>
<table border="1" cellpadding="10" cellspacing="2">
<thead>
<tr>
<th> User ID </th>
<th> <br /> </th>
</tr>
</thead>
<cfloop value="id" array="#form.userIDs#">
<tr>
<td>
#encodeForHtml( id )#
</td>
<td>
<!---
In order to make sure that the full set of user IDs is maintained
across the various FORM POSTS, we are going to include each ID as
a hidden input field. Then, on each form post, ColdFusion will
collapse all of the like-named fields into a single value. And,
since we using the "[]" suffix, that value will be an ARRAY.
--->
<input
type="hidden"
name="userIDs[]"
value="#encodeForHtmlAttribute( id )#"
/>
<!---
Each row in this table will have its own SUBMIT BUTTON. However,
since they are using unique Name/Value pairs, we can easily
determine which value the user was referring to (since we're
submitting the target ID as the VALUE).
--->
<button
type="submit"
name="removeUserID"
value="#encodeForHtmlAttribute( id )#">
Remove
</button>
</td>
</tr>
</cfloop>
</table>
<p>
<!---
A no-op post back, just to demonstrate that all of the hidden inputs will
correctly maintain the current list of IDs.
--->
<button type="submit">
Post Back
</button>
<a href="#cgi.script_name#">
Clear
</a>
</p>
</form>
</cfoutput>
Now, if we interact with this HTML FORM, we get the following output:
As you can see, by using all of the techniques that we outlined above, we're easily able to manage a list of user IDs using HTML FORM posts. And, because we never submit the list of IDs in the URL, we never have to worry about data-length limits imposed by some browsers.
It's awesome how easy these "old school" techniques can be. And, it's always a pleasure to work with HTML FORM data in ColdFusion. And, hopefully, there was something in this post that was new and exciting to newer ColdFusion developers.
Want to use code from this post? Check out the license.
Reader Comments
Even us old school developers need a reminder how to use basic HTML sometimes. I'm so accustom to using JS to solve what
<button value=''>
solves that I've completely forgotten about it. Thanks for the reminder!@Chris,
Heck yeah, teamwork! This is how I feel every time I remember that there's like 100 "semantic" HTML elements that I don't know about 😂
It should be noted, that any input with an empty string value, will simply NOT be included in the resulting list or array in the FORM scope. For example inputs with values 1, '' , 3 will submit as '1,3' or [1,3]. This is quite odd and might be unexpected to some.
Ray Camden wrote about this and recommended using Java getPageContext().getRequest().getParameterMap() to get the correct values of inputs:
https://www.raymondcamden.com/2014/02/25/ColdFusion-and-Form-Fields-with-the-Same-Name
Another option might be setting the input values to some placeholder value instead of empty string, just to make everything submit correctly.
@Ivan,
That's a great point. ColdFusion tends to not view empty list items as existing. Even the
List*
items will skip over empty items (unless told explicitly to include them). That said, I envision this technique, about managing IDs, as being something that happens more behind the scenes. Meaning, through hidden form fields. But, if I did have an input where someone could explicitly provide IDs (like I might do in an administrative UI), taking more precautions is totally the right move!@Ben,
Yes, the analogy with list functions is perfect. It means this behavior of ColdFusion is not odd but rather consistent. But it can cause issues. For example when submitting multiple hidden inputs representing some optional property or nullable db field.
@All,
Here is a follow-up post that polyfills the
name[]
form field grouping behavior for Adobe ColdFusion:www.bennadel.com/blog/4592-polyfill-form-field-grouping-using-bracket-notation-in-adobe-coldfusion.htm