Parsing AngularJS Request Data On The Server Using ColdFusion
By default, AngularJS posts all data to the server in JSON (JavaScript Object Notation) format. Unlike a regular "Form post," however, ColdFusion won't automatically parse the incoming request and add it to the Form scope. This means it's up to you to manually parse the data. Fortunately, it only takes a few lines of ColdFusion code.
To see this in action, I created a tiny AngularJS demo that posts data to the server using a subset of the HTTP methods: GET, POST, PUT, and DELETE. AngularJS will accept just about any "data" that you give it; however, I try to always pass an object. Even if that object has only one key, I find the consistency of a top-level object makes life a lot easier in the long run.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Parsing AngularJS Request Data On The Server Using ColdFusion
</title>
</head>
<body ng-controller="DemoController">
<h1>
Parsing AngularJS Request Data On The Server Using ColdFusion
</h1>
<!-- For each type of HTTP method, output the echoed result. -->
<div ng-repeat="dump in cfdumps">
<h3>
{{ dump.method }}
</h3>
<div ng-bind-html="dump.html">
<!-- To be populated with the CFDump from the server. -->
</div>
</div>
<!-- Initialize scripts. -->
<script type="text/javascript" src="../jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../angularjs/angular-1.2.4.min.js"></script>
<script type="text/javascript">
// Define the module for our AngularJS application.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the main demo.
app.controller(
"DemoController",
function( $scope, $http ) {
// I hold the data-dumps of the FORM scope from the server-side.
$scope.cfdumps = [];
// Make an HTTP request for each type of verb.
angular.forEach( [ "get", "post", "put", "delete" ], makeRequest );
// ---
// PRIVATE METHODS.
// ---
// I post data to the API using the given method type.
function makeRequest( method ) {
// AngularJS will try to work with ANY type of data that you pass
// in the "data" property. However, I try to ALWAYS pass an object
// with key-value pairs as I find that this makes the data easier
// to consume on the server.
var request = $http({
method: method,
url: "echo.cfm",
data: {
id: 4,
name: "Kim",
status: "Best Friend"
}
});
// Store the data-dump of the FORM scope.
request.success(
function( html ) {
$scope.cfdumps.push({
method: method.toUpperCase(),
html: html
});
}
);
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I override the "expected" $sanitize service to simply allow the HTML to be
// output for the current demo.
// --
// NOTE: Do not use this version in production!! This is for development only.
app.value(
"$sanitize",
function( html ) {
return( html );
}
);
</script>
</body>
</html>
This code posts to the server; then, the server parses the request data, adds it to the Form scope, and echoes the Form scope back to the client in the response (using CFDump).
<cfscript>
// Since we need to deserialize and consume the incoming request, there are a number
// of things that can go wrong.
try {
requestBody = toString( getHttpRequestData().content );
// Not every type of request will have a "body" (ex, "GET"). But, for this demo,
// we're going to assume the body, if it exists, will always be an object. I find
// an object easier to work with - I never let the client pass in a simple value
// or an array.
if ( len( requestBody ) ) {
structAppend( form, deserializeJson( requestBody ) );
}
} catch ( any error ) {
// The incoming request data was either not JSON or was not an "object". A this
// point, you might want to throw a custom error; or, you might want to let the
// request continue and defer processing logic to some controller.
// --
// throw( type = "BadRequest" );
}
// Echo the state of the Form scope (output on the client).
writeDump( var = form, label = "Form Scope" );
</cfscript>
As you can see, there's very little logic here. The incoming request body can be accessed using the getHttpRequestData() function. I chose to append the data to the existing Form scope; but, there's no rule that says you have to do that.
When we run the above code, we get the following output:
Again, you don't have to pass in an object. But, I find it creates a cleaner, more consistent solution; especially if you want to append the incoming data to the Form scope or some other type of "request collection," that many frameworks use.
Want to use code from this post? Check out the license.
Reader Comments
I think Angular sets the "Content-Type" request header to "application/json". To make your code more full proof, you could just add something to your onRequest event handler to look if the "Content-Type" header is "application/json" and if it is, then do your deserialization.
This would avoid potential issues w/normal form posts and you could add the base code into any site, so that JSON data as the content body always gets translated into the FORM scope.
@Dan,
Ah, very good idea! I just took a look at the source code and your are right - it looks like they use, "application/json;charset=utf-8".
And for others' reference, this can be accessed via:
getHttpRequestData().headers[ "content-type" ]
... of course, this header won't always exist, depending on the type of request. It looks like it only exists IF there is actual request content. As such, GET requests don't have a content-type.
Ben/Dan,
I added this to my onRequestStart() method in Application.cfc:
try {
// deserialize application/json type requests
if( getHTTPRequestData().headers["Content-Type"] eq "application/json" ){
requestBody = toString( getHttpRequestData().content );
if ( len( requestBody ) ) {
structAppend( form, deserializeJson( requestBody ) );
}
}
}
catch(any error) {}
However, in my handler i can't access any of the form data because it is missing.
But in dev tools I can see that the request payload is {"address":"9584 California Ave","city":"Riverside","state":"CA","zip":"92503"}
This post saved me. A few days back I couldn't get $http.post() to pass anything to the form scope so I reverted back to a jQuery Ajax call. Today I decided to give $http.post() another whirl. No matter what I couldn't get the form scope to receive any data. I did notice, however, this odd binary value in getHttpRequestData().content. Never would I have thought to convert that over using toString(). I can't thank you enough for sharing this. The only thing I had to add to my code was a Boolean condition isJSON(requestBody). In some instances JSON doesn't get passed and the call to deserializeJSON() was throwing an error.
I am new to angularjs and coldfusion components, so very slow at learning this.
I got my anuglar form to update using your example sending my data to a cfm file which works great, I would like a cfc solution. Is is possible to add the getHttpRequestData() to the init() of a component and use the structAppend( form) in any function in that component? Or am I barking up the wrong tree?