Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with:

Ask Ben: Automatically Generating Remote CFC Proxies In Javascript

Posted by Ben Nadel

Ben, I really like what you did with the javascript ajax proxy stuff this morning. In your post you mentioned that it wouldn't be hard to automate the generation of the javascript objects. I know this is a lot to ask, and you do so much already, but would you might demonstrating it? I wouldn't even know where to begin. thanks in advance!!

No problem at all. I'm a bit short on time today, so I am sorry if the explanation is a bit brief. Basically, what we want to do is take the static Javascript from my previous post and generate it based on the remote component meta data. ColdFusion component meta data contains all of the methods available in the target CFC; what we are going to end up doing is looping over that list of functions and outputting Javascript wrapper methods for each function that allows for remote access.

I've wrapped this functionality up into its own ColdFusion component, CFCProxy.cfc, but first, let's just look at an updated demo:

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Custom CFC Proxy Demo</title>
  •  
  • <script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.1.min.js"></script>
  • <script type="text/javascript">
  •  
  • <!--- Output the Javascript proxy classes. --->
  • <cfset objProxyService = CreateObject( "component", "CFCProxy" )
  • .Init()
  • .GenerateClass( "NameService" )
  • .GenerateClass( "AdjectiveService" )
  • .GenerateClass( "PartService" )
  • />
  •  
  •  
  • // Initialize the form when the document has loaded.
  • $(
  • function(){
  • // Create a new CFC proxy.
  • var objNameService = new NameService();
  • var objAdjectiveService = new AdjectiveService();
  • var objPartService = new PartService();
  •  
  •  
  • // Get the names.
  • objNameService.GetNames(
  • {},
  • function( objResponse ){
  • PopulateSelect( "name", objResponse );
  • }
  • );
  •  
  •  
  • // Hook up the onchange for the name.
  • $( "select[ name = 'name' ]" ).change(
  • function(){
  •  
  • // When the name changes, get the
  • // adjectives.
  • objAdjectiveService.GetAdjectives(
  • {},
  • function( objResponse ){
  • PopulateSelect( "adjective", objResponse );
  • }
  • );
  • }
  • );
  •  
  •  
  • // Hook up the onchange for the adjectives.
  • $( "select[ name = 'adjective' ]" ).change(
  • function(){
  •  
  • // When the adjective changes, get the
  • // body parts.
  • objPartService.GetParts(
  • {},
  • function( objResponse ){
  • PopulateSelect( "part", objResponse );
  • }
  • );
  • }
  • );
  •  
  • // Hook up the onchange for the parts.
  • $( "select[ name = 'part' ]" ).change(
  • function(){
  • DisplayMessage();
  • }
  • );
  • }
  • );
  •  
  •  
  •  
  • // This populates a given select box.
  • function PopulateSelect( strName, arrValues ){
  • var jSelect = $( "select[ name = '" + strName + "' ]" );
  •  
  • // Loop over the values and add as an option.
  • $.each(
  • arrValues,
  • function( intI, strValue ){
  • // Add option.
  • jSelect.append(
  • "<option value=\"" + strValue + "\">" +
  • strValue +
  • "</option>"
  • );
  • }
  • );
  • }
  •  
  •  
  • // I display the message based on the selections.
  • function DisplayMessage(){
  • jName = $( "select[ name = 'name' ]" );
  • jAdjective = $( "select[ name = 'adjective' ]" );
  • jPart = $( "select[ name = 'part' ]" );
  • jMessage = $( "#message" );
  •  
  • // Set message text.
  • jMessage.text(
  • "OMG! You think " +
  • jName.val() +
  • " has " +
  • jAdjective.val() +
  • " " +
  • jPart.val() +
  • ". Pervert!"
  • );
  • }
  •  
  • </script>
  •  
  • </head>
  • <body>
  •  
  • <h1>
  • Custom CFC Proxy Demo
  • </h1>
  •  
  • <form>
  •  
  • <p>
  • <select name="name">
  • <option value="">- -</option>
  • </select>
  •  
  • has
  •  
  • <select name="adjective">
  • <option value="">- -</option>
  • </select>
  •  
  • <select name="part">
  • <option value="">- -</option>
  • </select>
  • </p>
  •  
  • </form>
  •  
  •  
  • <p
  • id="message"
  • style="color: red ; font-weight: bold ; font-size: 20px ;">
  • <!-- Message will be injected here. -->
  • </p>
  •  
  • </body>
  • </html>

As you can see here, the page-relevant Javascript is all the same; but, the generating of the remote AJAX proxy Javascript classes has been factored out into our new ColdFusion component:

  • <!--- Output the Javascript proxy classes. --->
  • <cfset objProxyService = CreateObject( "component", "CFCProxy" )
  • .Init()
  • .GenerateClass( "NameService" )
  • .GenerateClass( "AdjectiveService" )
  • .GenerateClass( "PartService" )
  • />

This CFCProxy.cfc ColdFusion component knows how to generate the base Javascript object (only once) and each unique proxy wrapper class.

Let's take a look at what's going on under the hood:

  • <cfcomponent
  • output="false"
  • hint="I hanlde creating Javascript AJAX proxies.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized object.">
  •  
  • <!--- Set up instance variables. --->
  • <cfset VARIABLES.Instance = {
  • GeneratedClasses = {}
  • } />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="CacheClass"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I store the class name to flag it as being cached (generated).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="CFC"
  • type="string"
  • required="true"
  • hint="The path to the CFC for which we are creating a Javascript proxy."
  • />
  •  
  • <cfset VARIABLES.Instance.GeneratedClasses[ ARGUMENTS.CFC ] = true />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GenerateClass"
  • access="public"
  • returntype="any"
  • output="true"
  • hint="I generate the given class and return This reference for method chaingin.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="CFC"
  • type="string"
  • required="true"
  • hint="The path to the CFC for which we are creating a Javascript proxy."
  • />
  •  
  • <!---
  • Check to see if this class has already been
  • genererated. If so, then we just want to return out
  • as we don't want to generate more than once.
  • --->
  • <cfif THIS.IsClassGenerated( ARGUMENTS.CFC )>
  •  
  • <!--- Terminate call. --->
  • <cfreturn THIS />
  •  
  • </cfif>
  •  
  • <!---
  • Check to see if we need to generate the base class.
  • This needs to be done before any other class is
  • created so that they can make use of the base instance.
  • --->
  • <cfif NOT THIS.IsClassGenerated( "__RemoteCFC" )>
  •  
  • #THIS.GenerateBaseClass()#
  •  
  • </cfif>
  •  
  • <!--- Generate requested class. --->
  • #THIS.GenerateExtendingClass( ARGUMENTS.CFC )#
  •  
  • <!--- Return This. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GenerateBaseClass"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I generate teh base Javascript proxy class.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!--- Cache this class creation. --->
  • <cfset THIS.CacheClass( "__RemoteCFC" ) />
  •  
  • <!--- Define Javascript. --->
  • <cfsavecontent variable="LOCAL.Javascript">
  •  
  • // This is our base Remote CFC. It handles the core
  • // functionality for calling our CFCs from Javascript
  • // objects.
  • function __RemoteCFC(){
  • // The name of the CFC being called.
  • this.Name = "";
  •  
  • // Return this object.
  • return( this );
  • }
  •  
  • // This handles the remote calls to the CFCs.
  • __RemoteCFC.prototype.MakeRemoteCall = function(
  • strMethod,
  • objData,
  • fnSuccess,
  • fnError
  • ){
  • // Create a data struct and extend it with the
  • // method name and the data to be passed.
  • var objRemoteData = {};
  •  
  • // Extend the remote data set.
  • $.extend(
  • objRemoteData,
  • objData,
  • {
  • method: strMethod,
  • returnFormat: "json"
  • }
  • );
  •  
  • // Make the AJAX call to the remote method.
  • $.ajax(
  • {
  • type: "get",
  • url: (this.Name + ".cfc"),
  • data: objRemoteData,
  • dataType: "json",
  •  
  • // Bind success and failure methods.
  • success: fnSuccess,
  • error: fnError
  • }
  • );
  •  
  • // Return this for method chaining.
  • return( this );
  • }
  •  
  •  
  • // Create a new core remote object. We will need this
  • // to create the prototype chain such that the other
  • // proxy classes can extend this.
  • __objRemoteCFC = new __RemoteCFC();
  •  
  • </cfsavecontent>
  •  
  • <!--- Return the generated Javascript. --->
  • <cfreturn LOCAL.Javascript />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GenerateExtendingClass"
  • access="public"
  • returntype="string"
  • output="true"
  • hint="I generate an exending Javascript proxy class.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="CFC"
  • type="string"
  • required="true"
  • hint="The path to the CFC for which we are creating a Javascript proxy."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!--- Cache this class creation. --->
  • <cfset THIS.CacheClass( ARGUMENTS.CFC ) />
  •  
  • <!---
  • Get the meta data for the given CFC. To do this, we
  • are going to create a temporary instance of the CFC
  • to more easily gather meta data.
  • --->
  • <cfset LOCAL.Functions = GetMetaData(
  • CreateObject( "component", ARGUMENTS.CFC )
  • ).Functions
  • />
  •  
  • <!--- Get the name of the class. --->
  • <cfset LOCAL.Name = ListLast( ARGUMENTS.CFC, "." ) />
  •  
  •  
  • <!--- Define Javascript. --->
  • <cfsavecontent variable="LOCAL.Javascript">
  •  
  • // ------------------------------------------------- //
  •  
  •  
  • // Create a Javascript proxy for this given CFC.
  • function #LOCAL.Name#(){
  • this.Name = "#LOCAL.Name#";
  • }
  •  
  • // Extend the core CFC Proxy functionality.
  • #LOCAL.Name#.prototype = __objRemoteCFC;
  •  
  • <!--- Loop over methods to create wrappers. --->
  • <cfloop
  • index="LOCAL.Function"
  • array="#LOCAL.Functions#">
  •  
  • <!--- Check to see if thsi is remote. --->
  • <cfif (LOCAL.Function.Access EQ "remote")>
  •  
  • // Define remote method wrapper.
  • #LOCAL.Name#.prototype.#LOCAL.Function.Name# = function( objData, fnSuccess, fnError ){
  • this.MakeRemoteCall( "#LOCAL.Function.Name#", objData, fnSuccess, fnError );
  • }
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfsavecontent>
  •  
  • <!--- Return the generated Javascript. --->
  • <cfreturn LOCAL.Javascript />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="IsClassGenerated"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I determine if the given class has been generated.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="CFC"
  • type="string"
  • required="true"
  • hint="The path to the CFC for which we are creating a Javascript proxy."
  • />
  •  
  • <!--- Return its existence in the cache. --->
  • <cfreturn StructKeyExists(
  • VARIABLES.Instance.GeneratedClasses,
  • ARGUMENTS.CFC
  • ) />
  • </cffunction>
  •  
  • </cfcomponent>

Like I said, I don't have much time to go through this, but I think you can see how it works - you pass in a CFC class path and the CFCProxy.cfc checks to see if it has already generated this class before. It does this to make sure that there are no duplicate classes. If it has not yet generated a given class, it then grabs the meta data from a temporary instance of the target CFC. When gathering the meta data, we are creating an actual instance of the target CFC rather than calling GetMetaData() on the CFC class path only because it makes the returned data much easier to work with. Then, based on this meta data, I am outputting a Javascript class including all its remote methods.

I hope that helps!




Reader Comments

The one thing I am a bit fuzzy on is how the URL pathing vs. the CFC pathing gets resolved. My demo page and my CFCs were all in the same path. I am not that familiar with root-contextual pathing, so that would probably need to be tweaked.

Reply to this Comment

I would recommend using getComponentMetaData() rather than getMetaData() since it doesn't create an instance of your component.

Reply to this Comment

Ben thanks for another great tutorial here. I am experimenting with auto-generating lots of utility code like my CRUD cfcs, etc. on a fairly large application, and I'm so glad I found this post to help me generate those proxies faster. I was really excited about the AJAX stuff when CF8 came out, but since js frameworks by nature get updated more quickly than server frameworks, I have been working to roll my own cfajax framework. This just saved me a ton of time.

One little bug I noticed when testing the example code (using the CFCs from the previous example): each time a make a selection in a "parent" select field, the "child" has the value range appended again. Not a big deal as you quickly demonstrated proof of concept here which was the point!

Thanks again for your contributions to us little guys. :)

Reply to this Comment

@Kevin,

Glad to help. I am relatively new to the whole code generation concept; I experiment a lot with it, but haven't put it to too much use in production. Are you finding it really useful?

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.