HomePortals/ColdBox Integration Revisited

A while ago I wrote a post about how to integrate the HomePortals layout rendering features into an a ColdBox application. Since then a lot has changed on both the HomePortals and ColdBox camps so I've been wanting to revisit that experiment and see if it could be made in an easier way now, using the advances on both frameworks. Read on for the findings.

The goal here is to have a fully functional ColdBox applications in which some pages are rendered using the HomePortals engine instead of the normal layout/view rendering of ColdBox.

To begin we need the following:

- ColdBox 3.0 Beta 2

- HomePortals 3.1.475 (or later)

Install both under /coldbox and /homePortals respectively (or using the appropriate CF mappings).

The application we will tackle will be 'simple' application template (this is found in the coldbox install directory).

Create a directory named hpcbox and copy in there the sample application template.

HomePortals works as a singleton, that means that we need to initialize and persist a single instance of it through out the application life. For this we will use the integrated model factory in ColdBox to manage our HomePortals instance. In ColdBox-speak we will need to add the HomePortals 'components' directory as an "external model location" in the main ColdBox config file.

/hpcbox/config/coldbox.xml.cfm

...
<Models>
<ExternalLocation>homePortals.components</ExternalLocation>
</Models>
...

This will allow us to get an instance of any component under homePortals/components/ just by doing a getModel() call.

The next thing we need to do is create an event handler to serve the HomePortals page. Remember, what we want is to have both normal and homePortals-based pages living togehter on the same app.

Here is the listing for the event handler:

/hpcbox/handler/cms.cfc

<cfcomponent extends="coldbox.system.EventHandler" output="false">

   <cfset variables.APP_ROOT = "/hpcbox">

   <cffunction name="preHandler" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfargument name="action" hint="The intercepted action"/>
      <cfif not getColdboxOCM().lookup("homePortalsEngine")>
         <cfset getColdboxOCM()
            .set("homePortalsEngine",
               getModel("homePortals")
                  .init( variables.APP_ROOT )
            )>

      </cfif>
   </cffunction>

   <cffunction name="onMissingAction" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfargument name="MissingAction" required="true" hint="The requested action string"/>
      <cfset arguments.event.setValue("hp.page",arguments.missingAction)>
      <cfset arguments.event.setLayout("Layout.HomePortals")>
   </cffunction>

</cfcomponent>

Our handler has two methods only, the first is the preHandler() event that will execute for every request to any event on this handler, and the second is the onMissingAction() that will execute whenever coldbox cannot find a method definition for a requested action. Since we are not defining any other methods, pretty much the onMissingAction will be executed for every requested event on this handler.

The preHandler() method uses the ColdBox caching features to see if we have a cached instance of "homePortalsEngine". If not, then asks the model factory for a new instance of the HomePortals engine, initializes it with the path to the application and stores it in the cache. This is the homePortals singleton that we will use to render our pages.

Note To Any ColdBox Gurus out there: I wanted to have the model factory do the persistence and initialization for me but couldn't figure it out how to do it without making changes to the original component (homePortals.cfc). That is why I had to explicitly put my instance on the cache. If you have any tips they will be very welcome.

The onMissingAction() method is very simple, it only sets a value named "hp.page" on the request collection with the same name of the requested action, and then overrides the default layout to use a special layout that we named "Layout.HomePortals" which we will use to handle our homePortals rendering.

This way if we want to display a HomePortals page named "mysuperpage", all we need to do is make a request like:

http://localhost/hpcbox/index.cfm?event=cms.mysuperpage

Finally we need to do our custom layout:

/hpcbox/layouts/Layout.HomePortals.cfm

<cfset page = event.getValue("hp.page")>
<cfset pageRenderer = getColdboxOCM().get("homePortalsEngine").load(page)>
<cfoutput>#pageRenderer.renderPage()#</cfoutput>

This couldn't be any simpler. We get our page name from the request collection ("hp.page"), get the HomePortals engine instance from the cache, and then tell HomePortals to render the HTML.

That's it for the ColdBox side of the equation, now we need to deal with HomePortals side.

First, we need to provide a config file for HomePortals. We do this by creating a file named homePortals-config.xml.cfm and placing it on the config/ directory of our app:

/hpcbox/config/homePortals-config.xml.cfm

<?xml version="1.0" encoding="UTF-8"?>
<homePortals>
   <contentRoot>/hpcbox/pages/</contentRoot>

   <renderTemplates>
      <renderTemplate name="page" type="page" href="/hpcbox/layouts/Layout.Page.htm" default="true" />
   </renderTemplates>

</homePortals>

Here we tell HomePortals that we will use the /hpcbox/pages directory to store our pages. Also we define a custom page template to handle the overall HTML structure. This is where we will indicate where to put the header, the sidebar, etc.

As you can see from the xml declaration the page template is just an HTML file. We put this file on the Layouts directory to keep the same ColdBox conventions, and also because both types of layouts fulfill the same goal (although with different implementation), which is to provide the top level structure or decoration of the page's main content.

Here is the template: /hpcbox/layouts/Layout.Page.htm:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html>
   <head>
      <title>$PAGE_TITLE$</title>
      <meta name="generator" content="HomePortals Portal Framework" />
      <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
      <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
      <link href="style.css" rel="stylesheet" type="text/css">
      $PAGE_HTMLHEAD$
   </head>
   <body>
      $PAGE_LAYOUTSECTION["HEADER"]["DIV"]$
      <table cellpadding="10" width="98%" align="center">
       <tr>
       <td valign="top">
            $PAGE_LAYOUTSECTION["MAIN"]["DIV"]$
       </td>
       <td valign="top" id="sidebar">
            $PAGE_LAYOUTSECTION["SIDEBAR"]["DIV"]$
       </td>   
       </tr>
       </table>
   </body>
</html>

It may not be obvious at first glance but this template follows the same HTML structure of the homepage of the original codbox application template that we used. It basically has a header, a main column for content and a sidebar.

Now for the last piece of the puzzle: the page to be displayed:

We already declared that our HomePortals pages will be stored on the /pages directory, and also that the page name is given by the event called on the handler. ColdBox uses the "index" event when no explicit event has been requested on a handler. So we will create a page named "index":

/hpcbox/pages/index.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Page>
<title>HomePortals/ColdBox App</title>
<layout>
   <location id="mainRegion" name="main" type="main"/>
   <location id="sidebarRegion" name="sidebar" type="sidebar"/>
</layout>
<body>
    <rss id="r1"
      rssurl="http://www.riaforge.org/index.cfm?event=page.rss"
      title="RIAForge News"
      showRSSTitle="false"
      location="main" />


    <image id="img1"
      href="http://www.homeportals.net/images/hp3_label.gif"
       location="sidebar"
       link="http://www.homeportals.net" />

</body>
</Page>

In this page we name our two content regions, and place a widget on each one. We use an RSS reader for the main content, and an image for the sidebar. Remember that these are part of the widgets that come standard with HomePortals. Here is an screenshot of the page:

Without much work ColdBox apps can benefit from a whole different set of reusable widgets provided by HomePortals. Additionally since the page was requested through a regular ColdBox event handler, you have all the ColdBox bells and whistles at your disposal.

Ok, that was good, but the integration so far is still not too tight. I mean, you have ColdBox views on one side, and HomePortals views on the other. So let's do something to make them become even more integrated.

Let's add ColdBox event requests as elements on a HomePortals page.

Ehhh... what? Let me explain, you saw how on the homePortals page we used the < rss > and < image > tags to display widgets or modules on the page, right? Well, a nice aspect of HomePortals is that you can write your own content tag renderers (that's what they are officially called). Then you can use your own renderers on your pages, move them around, call them multiple times, do whatever you want with them.

We will create a content tag renderer to encapsulate a ColdBox event handler request. So our tag will include an "event" attribute that will point to any ColdBox event action, it will then follow (almost) the same process lifecycle of a regular event and then display whatever view is set by the event. All encapsulated on the little space provided to the widget on the page. Moreover, we will be able to have multiple of these tag handlers on the same page, each one making independent requests from each other.

We will call our renderer "widgetEventHandler".

The code for the renderer is a bit more complicated, but basically it does the following:

• Extends "homePortals.components.contentTagRenderer" • Implements a method named "renderContent" • Replicates a customized version of the process coldbox follows when processing any event

Here is the code: Warning: ColdBox authors may have a heart attack just by looking at how I attrociously bastardized their pretty code :)

/hpcbox/widgetEventHandler.cfc

<cfcomponent extends="homePortals.components.contentTagRenderer">
   <cfproperty name="event" default="" type="string" displayname="Event" hint="Event to execute">

   <cffunction name="renderContent" access="public" returntype="void" hint="sets the rendered output for the head and body into the corresponding content buffers">
      <cfargument name="headContentBuffer" type="homePortals.components.singleContentBuffer" required="true">   
      <cfargument name="bodyContentBuffer" type="homePortals.components.singleContentBuffer" required="true">
      <cfscript>
         var html = "";
         var reqState = structNew();
         var nodeAttr = getContentTag().getModuleBean().toStruct();

         // create a structure to hold current request state          reqState = duplicate(form);
         StructAppend(reqState, url);
         StructAppend(reqState, nodeAttr);

         // process ColdBox request and generate output          html = processColdBoxRequest(reqState);
         
         // append HTML to output buffer          arguments.bodyContentBuffer.set( html );
      </cfscript>
   </cffunction>

   <cffunction name="processColdBoxRequest" access="private" returntype="string" hint="Process a Coldbox Request. Returns the generated output" output="true" >
      <cfargument name="reqState" type="struct" required="true">
      
      <cfset var cbController = 0>
      <cfset var Event = 0>
      <cfset var ExceptionService = 0>
      <cfset var ExceptionBean = 0>
      <cfset var renderedContent = "">
      <cfset var eventCacheEntry = 0>
      <cfset var interceptorData = structnew()>
      
      <cfset var appHash = hash(getBaseTemplatePath())>
      <cfset var lockTimeout = 30>
      
      <!--- Start Application Requests --->
      <cflock type="readonly" name="#appHash#" timeout="#lockTimeout#" throwontimeout="true">
         <cfset cbController = application.cbController>
      </cflock>
         
      <!--- set request time --->
      <cfset request.fwExecTime = getTickCount()>
      
      <!--- Create Request Context & Capture Request --->
      <cfset Event = cbController.getRequestService().requestCapture()>
      
      <!--- Override/append the request context --->
      <cfset Event.collectionAppend(arguments.reqState, true)>
      
      <!--- Execute preProcess Interception --->
      <cfset cbController.getInterceptorService().processState("preProcess")>
      
      
      <!--- IF Found in config, run onRequestStart Handler --->
      <cfif cbController.getSetting("RequestStartHandler") neq "">
         <cfset cbController.runEvent(cbController.getSetting("RequestStartHandler"),true)>
      </cfif>
      
      <!--- Before Any Execution, do we have cached content to deliver --->
      <cfif Event.isEventCacheable() and cbController.getColdboxOCM().lookup(Event.getEventCacheableEntry())>
         <cfset renderedContent = cbController.getColdboxOCM().get(Event.getEventCacheableEntry())>
      <cfelse>
      
         <!--- Run Default/Set Event not executing an event --->
         <cfif NOT event.isNoExecution()>
            <cfset cbController.runEvent(default=true)>
         </cfif>
      
         <!--- Check for Marshalling and data render --->
         <cfif isStruct(event.getRenderData()) and not structisEmpty(event.getRenderData())>
            <cfset renderedContent = cbController.getPlugin("Utilities").marshallData(argumentCollection=event.getRenderData())>
         <cfelse>
            <!--- Render View pair via set variable to eliminate whitespace--->
            <cfset renderedContent = cbController.getPlugin("Renderer").renderView()>
         </cfif>
         
         <!--- PreRender Data:--->
         <cfset interceptorData.renderedContent = renderedContent>
         <!--- Execute preRender Interception --->
         <cfset cbController.getInterceptorService().processState("preRender",interceptorData)>
         <!--- Replace back Content --->
         <cfset renderedContent = interceptorData.renderedContent>
         
         
         <!--- Check if caching the content --->
         <cfif event.isEventCacheable()>
            <cfset eventCacheEntry = Event.getEventCacheableEntry()>
            <!--- Cache the content of the event --->
            <cfset cbController.getColdboxOCM().set(eventCacheEntry.cacheKey,
                                          renderedContent,
                                          eventCacheEntry.timeout,
                                          eventCacheEntry.lastAccessTimeout)>

         </cfif>
         
         <!--- Render Content Type if using Render Data --->
         <cfif isStruct(event.getRenderData()) and not structisEmpty(event.getRenderData())>
            <!--- Render the Data Content Type --->
            <cfcontent type="#event.getRenderData().contentType#; charset=#event.getRenderData().encoding#" reset="true">
            <!--- Remove panels --->
            <cfsetting showdebugoutput="false">
         </cfif>
         
         
         <!--- Execute postRender Interception --->
         <cfset cbController.getInterceptorService().processState("postRender")>
         
         
         <!--- If Found in config, run onRequestEnd Handler --->
         <cfif cbController.getSetting("RequestEndHandler") neq "">
            <cfset cbController.runEvent(cbController.getSetting("RequestEndHandler"),true)>
         </cfif>
         
         <!--- Execute postProcess Interception --->
         <cfset cbController.getInterceptorService().processState("postProcess")>
         
      
      <!--- End else if not cached event --->
      </cfif>
      
      <cfreturn renderedContent>
   </cffunction>

</cfcomponent>

Finally we need to tell HomePortals about our new tag renderer. To do this we append the following section to our homePortals-config.xml.cfm file:

/hpcbox/config/homePortals-config.xml.cfm

...
<contentRenderers>
<contentRenderer moduleType="widget" path="hpcbox.widgetEventHandler" />
</contentRenderers>
...

And then we modify our index.xml page to use the new tag:

/hpcbox/pages/index.xml

<Page>
<title>HomePortals/ColdBox App</title>
<layout>
<location id="headerRegion" name="header" type="header"/>
      <location id="mainRegion" name="main" type="main"/>
      <location id="sidebarRegion" name="sidebar" type="sidebar"/>
</layout>
<body>
   <widget id="w0" event="widgets.header" title="" location="header" moduleTemplate="moduleNoContainer" />
   <widget id="w4" event="widgets.registeredEventHandlers" location="main" />
   <widget id="w1" event="widgets.gettingStarted" location="main" />
   <widget id="w2" event="widgets.docSearch" title="Docs Search" location="sidebar" />
   <widget id="w3" event="widgets.links" title="Community Links" location="sidebar" />
   <rss id="r1"
      rssurl="http://www.riaforge.org/index.cfm?event=page.rss"
      title="RIAForge News"
      showRSSTitle="false"
      location="main" />


   <image id="img1"
      href="http://www.homeportals.net/images/hp3_label.gif"
       location="sidebar"
       link="http://www.homeportals.net" />

</body>
</Page>

Note that our widget tags are calling a new event handler named widgets. Which is defined as follows:

/hpcbox/handlers/widgets.cfc

<cfcomponent extends="coldbox.system.EventHandler" output="false">
   
   <cffunction name="gettingStarted" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfset event.setView("widgets/gettingStarted")>
   </cffunction>

   <cffunction name="docSearch" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfset event.setView("widgets/docSearch")>
   </cffunction>

   <cffunction name="links" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfset event.setView("widgets/links")>
   </cffunction>

   <cffunction name="header" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfset var rc = event.getCollection()>
      <cfset event.setValue("welcomeMessage","Welcome to ColdBox! (+HomePortals)")>   
      <cfset event.setView("widgets/header")>
   </cffunction>

   <cffunction name="registeredEventHandlers" returntype="void" output="false">
      <cfargument name="event" required="true">
      <cfset event.setView("widgets/registeredEventHandlers")>
   </cffunction>

</cfcomponent>

I won't list the code for the views here but they are regular ColdBox views, using the normal ColdBox features available. You can find the entire code on the download at the end of this post.

So here is the final result:

So that's it. I showed how you can enrich your ColdBox applications using the layout and widget management features of the HomePortals framework. Take the time to explore the new HomePortals, I'm sure you will find some interesting features to improve your applications, especially when dealing with modular content.

Oh and more thing!

Once you have your app setup like this, why not throw a little CMS to manage your content without modifying your Coldbox site at all?

Stay tuned for more HomePortals integration on other frameworks too!

PS: You can find the sample app attached to this post.

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
BlogCFC was created by Raymond Camden. This blog is running version 5.9. Contact Blog Owner