As I blogged last week, AngularJS provides the $resource class as an abstraction layer between your client-side code and the server-side API. This makes performing CRUD-style operations across the network fairly easy. But what happens when you need to execute a command on a RESTful resource that falls outside the standard CRUD (ie. Create, Read, Update, Delete) methods? Luckily, AngularJS handles RESTful "controllers" quite nicely.
According to The REST API Design Rulebook by Mark Masse, there are four resource archetypes:
Document, Collection, and Store are all about representing resources. Controllers, on the other hand, are about mutating resources. You can already use HTTP verbs to mutate a resource; but, sometimes, run-of-the-mill CRUD operations don't quite make sense. Take, as an example, the following resources:
Given this Collection of messages (first resource) and an individual message (second resource), how would you go about clearing all messages? Or, how about archiving the given message? CRUD doesn't quite fit the bill. But, Controllers work perfectly for these types of operations:
Here, we're using the "clear-all" Controller and the "archive" Controller to mutate the Collection and the Document representations, respectively.
When we start using Controllers in our resources, we are now left with a URL schema that has more variability. AngularJS $resource's can handle this; but, you have to get a little "clever" with how you define your parameter bindings.
Pulling the messages concept from above into an AngularJS context, we'd have to define our resource using the following template:
The problem with this is that there is now a slight ambiguity around the first URL parameter. Is ":id" referring to my Collection-based Controller? Or is it referring to my Document ID?
To clear up this ambiguity, AngularJS allows us to define multiple parameters in the same portion of the URL template:
Notice that the second item in my URL template contains two parameters:
As long as I only use one of these at a time, AngularJS will construct the RESTful resource properly. To demonstrate, I've put together a demo that defines and then invokes the messages resource from above:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>Using RESTful Controllers In AngularJS Resources</title>
<!--
Because we are working with Resources, we have to load both
AngularJS and the ngResource module.
-->
<script type="text/javascript" src="../angular-1.0.2/angular.js"></script>
<script type="text/javascript" src="../angular-1.0.2/angular-resource.js"></script>
<script type="text/javascript">
// Tell Angular to load the ngResource before loading our
// custom app module.
var app = angular.module( "Demo", [ "ngResource" ] );
// Run this when the app is ready.
app.run(
function( $resource ) {
// When defining the resource, we get a few actions
// out of the box, like get() and query(), based on
// standard HTTP verbs. But, we can also use RESTful
// controller to change resource state using an
// action that falls outside normal CRUD operations.
var messages = $resource(
"./api.cfm/messages/:listController:id/:docController",
{
id: "@id",
listController: "@listController",
docController: "@docController"
},
{
clear: {
method: "POST",
params: {
listController: "clear-all"
}
},
archive: {
method: "POST",
params: {
docController: "archive"
}
}
}
);
// Now that we have our resource defined, let's invoke
// it with various configurations.
// GET without ID.
messages.query();
// Post with our list controller.
messages.clear();
// GET with ID.
messages.get(
{
id: 4
}
);
// POST with our document "controller".
messages.archive(
{
id: 8
}
);
}
);
</script>
</head>
<body>
<!-- Left intentionally blank. -->
</body>
</html>
Notice that my AngularJS resource accounts for two different controllers:
When I invoke the resource methods above, I end up making four requests to the following URLS:
As you can see AngularJS properly negotiated the appropriate RESTful URLs. Hella sweet!
If you are interested, here is my test API file:
api.cfm<!--- Get the raw resourced path that was requested. --->
<cfset resourcePath = cgi.path_info />
<!---
NOTE: ColdFusion 10 seems to hang until I make a request to get
the POST body. Not sure why.
--->
<cfif ( cgi.request_method neq "GET" )>
<cfset requestBody = getHTTPRequestData().content />
</cfif>
<!---
Identify the type of request that has come in based on the
pattern of the requested resource path.
--->
<cfif reFind( "^/messages$", resourcePath )>
<cfset response = "GET without ID." />
<cfelseif reFind( "^/messages/clear-all$", resourcePath )>
<cfset response = "POST with clear-all Controller." />
<cfelseif reFind( "^/messages/\d+$", resourcePath )>
<cfset response = "GET with ID." />
<cfelseif reFind( "^/messages/\d+/archive+$", resourcePath )>
<cfset response = "POST with archive controller" />
<cfelse>
<cfset response = "Hmm, couldn't match resource." />
</cfif>
<!---
Serialize the response as JSON. AngularJS will know how to
parse this.
--->
<cfset serializedResponse = serializeJSON( response ) />
<!--- Add header for debugging the path. --->
<cfheader
name="X-Debug-Path"
value="#cgi.path_info#"
/>
<!--- Stream response back to the client. --->
<cfcontent
type="application/json"
variable="#charsetDecode( serializedResponse, 'utf-8' )#"
/>
For some reason, ColdFusion 10 was hanging on POST requests until I accessed the request body (via the getHttpRequestData() function). This only happened if the request body contained JSON (JavaScript Object Notation) data. If it was empty, the request did not hang. I don't know enough about request processing to know if that makes any sense. I can, however, confirm that ColdFusion 9 does not do this.
Want to use code from this post? Check out the license.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4