Skip to main content
Ben Nadel at the ColdFusion Centaur And Bolt User Group Tour (Jun. 2009) with: Ben Forta
Ben Nadel at the ColdFusion Centaur And Bolt User Group Tour (Jun. 2009) with: Ben Forta

Associating Form Inputs With ColdFusion Validation Error Types

By
Published in , ,

In my ColdFusion applications, I've never have a lot of ceremony around error handling. I simply try to catch errors as high-up in the stack as I can; and then, I use a centralized error translator to translate exceptions into a user-safe error response which I then render at the top of my form interface. It recently occurred to me that I might be able to use my user-safe error response to make my ColdFusion forms more accessible by marking form inputs as being related to certain server-side validation errors.

As much as possible, I try to keep my forms as short as possible. Which means that when I render an error message such as:

"Please enter a valid email address."

... it should be immediately clear to the user which form control is related to the given error. After all, it's probably one of only 2 or 3 inputs on the screen.

But, mapping the rendered error message onto the form input does come with some cognitive load. And, I can probably make it a little easier for the user by loosely associating one-or-more controls with the error.

To do this, I can allow each form control to provide a list of error types that might be relevant. This way, the form experience is progressively enhanced to provide more insight; but, will gracefully degrade if the server-side changes the way it does error translation.

Before we look at the client-side code, let me show you how I currently handle my ColdFusion validation. For the sake of this thought experiment, we're going to create a new User account. At the root of this workflow, I have a service component (UserService.cfc) that represents a "User Entity". The role of this service is ensure the integrity of the data pertaining to the concept of a "user" within the system:

component
	output = false
	hint = "I provide service methods for the user entity."
	{

	/**
	* I create a user with the given properties. If inputs are not valid, a validation
	* error is thrown.
	*/
	public numeric function createUser(
		required string name,
		required string email,
		required string password
		) {

		if ( ! name.len() ) {

			throw( type = "App.Model.User.Name.Empty" );

		}

		if ( name.len() > 20 ) {

			throw( type = "App.Model.User.Name.TooLong" );

		}

		if ( ! email.reFind( "^[^@]+@[^.]+(\.[^.]+)+$" ) ) {

			throw( type = "App.Model.User.Email.Invalid" );

		}

		if ( email.len() > 75 ) {

			throw( type = "App.Model.User.Email.TooLong" );

		}

		if ( password.len() < 10 ) {

			throw( type = "App.Model.User.Password.TooShort" );

		}

		// Todo: create user entity, not the point of this demo.

		return 1;

	}

}

For this demo, this ColdFusion component doesn't do anything other than throw errors when we invoke the createUser() method with invalid data. Notice that each error type is unique. This is the error type that we're going to return to the user interface:

  • App.Model.User.Name.Empty
  • App.Model.User.Name.TooLong
  • App.Model.User.Email.Invalid
  • App.Model.User.Email.TooLong
  • App.Model.User.Password.TooShort

When one of these errors is thrown, we don't want to reveal the raw error information to the end-user. Instead, we want to translate this application-error into a user-safe error. For that, I use a centrally located translation service (ErrorTranslator.cfc). This ColdFusion component performs a switch on the error.type and returns a payload that represents a user-safe error response:

component
	output = false
	hint = "I help translate application errors into appropriate response codes and user-facing messages."
	{

	/**
	* I return a normalized, user-safe error response for the given server-side error.
	*/
	public struct function getResponse( required any error ) {

		switch ( error.type ) {
			case "App.Model.User.Conflict":
				return as422({
					type: error.type,
					message: "That email address is already in use. Please use a different email address."
				});
			break;
			case "App.Model.User.Email.Invalid":
				return as422({
					type: error.type,
					message: "Please enter a valid email address."
				});
			break;
			case "App.Model.User.Email.TooLong":
				return as422({
					type: error.type,
					message: "Your email address is too long. It must be less than 75 characters."
				});
			break;
			case "App.Model.User.Name.Empty":
				return as422({
					type: error.type,
					message: "Please provide your full name."
				});
			break;
			case "App.Model.User.Name.TooLong":
				return as422({
					type: error.type,
					message: "Your name is too long. It must be less than 20 characters."
				});
			break;
			case "App.Model.User.Password.TooShort":
				return as422({
					type: error.type,
					message: "For security purposes, your password must be longer than 10 characters."
				});
			break;
			default:
				// Demo should never get here.
				writeDump( error );
				abort;
			break;
		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I generate a 422 response with the given overrides.
	*/
	private struct function as422( required struct overrides ) {

		var response = {
			statusCode: 422,
			statusText: "Unprocessable Entity",
			type: "App.UnprocessableEntity",
			title: "Unprocessable Entity",
			message: "Your request cannot be processed in its current state. Please validate the information in your request and try submitting it again."
		};

		return response.append( overrides );

	}

}

In this deeply abbreviated example, I'm passing each error.type value through to the user-safe error response. It's up to the application to determine which type values are "safe" for the user to know about. If an error type should be kept private, the type property can be overridden during this translation operation.

Now that you understand how I'm both validating data and translating errors on the ColdFusion side, let's look at a very simple account creation form. In the following ColdFusion page, the user must provide three data-points:

  • Name
  • Email
  • Password

Some of these fields may result in multiple, unique error type values. I'm going to use an HTML data- attribute to define the list of error prefixes that might be relevant to the given form control. For example, on the password <input>, I'll include:

data-error-types="App.Model.User.Password."

Then, when the ColdFusion page renders, I'll use some client-side JavaScript to query the rendered DOM (Document Object Model) and look for inputs that reference the current error type. In this demo, I'm going to add an is-error CSS class that will help to highlight the field for better insight.

This simple form performs a POST back to itself. The form submission is processed at the top of the CFML template and the resultant errorResponse is used to render both an error message above the form and the aforementioned JavaScript below the form.

<cfscript>

	param name="form.name" type="string" default="";
	param name="form.email" type="string" default="";
	param name="form.password" type="string" default="";
	param name="form.submitted" type="boolean" default=false;

	userService = new UserService();
	errorTranslator = new ErrorTranslator();
	errorResponse = "";

	if ( form.submitted ) {

		try {

			// Not all validation can be done at the "entity service" layer. Some
			// validation must be performed across entities and must be performed at a
			// higher level in the application. For the demo, we'll just hard-code a
			// uniqueness check for email.
			if ( form.email == "ben@test.com" ) {

				throw( type = "App.Model.User.Conflict" );

			}

			newID = userService.createUser(
				name = form.name.trim(),
				email = form.email.lcase().trim(),
				password = form.password.trim()
			);

			// Yay, user created successfully (not the point of this exploration).

		} catch ( any error ) {

			errorResponse = errorTranslator.getResponse( error );

		}

	}

</cfscript>
<cfoutput>

	<!doctype html>
	<html lang="en">
	<head>
		<meta charset="utf-8">
		<link rel="stylesheet" type="text/css" href="./main.css">
		<style type="text/css">

			.is-error {
				box-shadow: inset 0px 0px 0px 4px hotpink ;
			}

		</style>
	</head>
	<body>

		<h1>
			Sign-up Form
		</h1>

		<!--- If there is a server-side error response, render the user-safe message. --->
		<cfif isStruct( errorResponse )>
			<p role="alert" aria-live="assertive">
				#encodeForHtml( errorResponse.message )#
			</p>
		</cfif>

		<form method="post" action="./test.cfm">
			<input type="hidden" name="submitted" value="true" />

			<p>
				<label for="target--name">
					Name:
				</label>
				<input
					id="target--name"
					type="text"
					name="name"
					value="#encodeForHtmlAttribute( form.name )#" data-1p-ignore
					data-error-types="
						App.Model.User.Name.Empty
						App.Model.User.Name.TooLong
					"
				/>
			</p>
			<p>
				<label for="target--email">
					Email:
				</label>
				<input
					id="target--email"
					type="text"
					name="email"
					value="#encodeForHtmlAttribute( form.email )#" data-1p-ignore
					data-error-types="
						App.Model.User.Email.
						App.Model.User.Conflict
					"
				/>
			</p>
			<p>
				<label for="target--password">
					Password:
				</label>
				<input
					id="target--password"
					type="password"
					name="password" data-1p-ignore
					data-error-types="App.Model.User.Password."
				/>
			</p>
			<p>
				<button type="submit">
					Create Account
				</button>
			</p>
		</form>

		<!---
			If there's a server-side processing error, we need to serialize it into the
			client-side code such that we can query the DOM and look for inputs that
			appear to match the server-side validation error.
		--->
		<cfif isStruct( errorResponse )>
			<script type="text/javascript">

				var errorResponse = JSON.parse( "#encodeForJavaScript( serializeJson( errorResponse ) )#" );
				var formControls = document.querySelectorAll( "[data-error-types]" );

				for ( var control of formControls ) {

					// If any of the embedded prefixes appear in the server-side error
					// type, mark this form control as being error-related.
					for ( var prefix of extractPrefixes( control.dataset.errorTypes ) ) {

						if ( errorResponse.type.startsWith( prefix ) ) {

							control.classList.add( "is-error" );
							break;

						}

					}

				}


				/**
				* I break the [data-error-types] attribute into an array of types.
				*/
				function extractPrefixes( dataAttribute ) {

					return dataAttribute.split( /[,\s]+/g )
						.filter( match => match )
					;

				}

			</script>
		</cfif>

	</body>
	</html>

</cfoutput>

As you can see in the bottom of the page, I'm serializing the errorResponse into the JavaScript context. Then, I'm matching the errorResponse.type value against all of the prefixes defined within the data- attributes. And, if any match, I append the is-error CSS class.

Now, if I run this ColdFusion page and submit the form with progressively more information, I see the following experience:

Screen recording of a user submitting form with incomplete data. With each successive submission, a different form control is highlighted.

Notice that in addition to the error message that is rendered at the top, the is-error CSS class adds an inset shadow to the relevant, problematic form field.

Using a shadow to show validation errors is problematic for people with color-blindness; so, I'd likely need to find a different (or additional) way to signal the issue. But this exploration was less about the rendering of the error and more about the association of the form controls to the ColdFusion error response.

I'm liking this technique because:

  1. It's completely optional. As long as I always render the user-safe error response at the top, the form is usable and the errors should be relatively easy to debug. As I mentioned earlier, I keep my forms rather small; so, any objection that entails, "What if I have a form with 200 inputs on it" is immediately irrelevant for 99.999% of all websites.

  2. It degrades gracefully. If the error type values change, I may lose some of the field-specific targeting; but, the error response is always rendered at the top. As such, the form remains usable.

  3. It's very simple. I don't have to deal with configuration files or build steps that introspect the database or any nonsense like that.

Right now, I'm dynamically injecting a CSS class. But, this might not be sufficient in the long run. I could always move from a client-side rendering to a server-side rendering using something like a ColdFusion module / custom tag. Imagine having an explicit tag next to each input:

<p>
	<label for="target--email">
		Email:
	</label>
	<input
		id="target--email"
		type="text"
		name="email"
		value="#encodeForHtmlAttribute( form.email )#" data-1p-ignore
	/>

	<!--- Will only render if the prefixes match. --->
	<cfmodule
		template="/tags/field-error.cfm"
		errorResponse="#errorResponse#"
		prefixes="#[
			'App.Model.User.Email.',
			'App.Model.User.Conflict'
		]#"
	/>
</p>

Clearly this is a lot more work than a generically applied CSS class. But, it would allow for a much more robust implementation.

And, I'm sure there are myriad of other approaches (such as using an HTML <template> tag instead of a CFModule tag). The goal here isn't to find the perfect solution (spoiler alert, one doesn't exist); the goal here was really to see if I could find a solution that loosely ties a form control to an error type returned by the ColdFusion server. And that seems quire reasonable to me.

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

Reader Comments

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