Wednesday, May 27, 2009

An alternative Navbar system for Orbeon Forms

Working with the Orbeon Forms system, I was confronted with the issue of localizing the navbar. Out of the box, the Orbeon implementation does not support the localization of the navbar appearing on the left-hand side of the demo application. I decided to bite the bullet and implement an alternative navbar to replace the one base on apps-list.xml. Here is a summary description for those who may me interested.

First, to support localization, the navbar structure had to be separate from the localized content, a rule that the apps-list.xml implementation does not follow.

Second, I wanted to support an arbitrary menu depth so we can have a top level and sub levels and sub-sub levels etc.

Third, the presentation had to be independent of the structure so if someone prefers a horizontal menu with drop-downs or a navigation tree, they could implement the UI and still use the defined menu structure and localization.

To start, I defined a menu structure in a file I called "nav.xml". Here is an excerpt from such a file:
<menu>
<title ref="navbar/title"/>
<section id="home" href="/welcome/">
<label ref="navbar/home/label" />
<hint ref="navbar/home/hint"/>
<item href="/documentation/">
<label ref="navbar/Documentation" />
<hint/>
</item>
</section>

<section id="manage">
<label ref="navbar/manage/label" />
<item id="manage.wo" href="/wfdemo/manage/wo-manager/">
<label ref="navbar/manage/WorkOrders/label" />
<hint ref="navbar/manage/WorkOrders/hint"/>
</item>
<item id="manage.assign" href="/wfdemo/manage/assign/">
<label ref="navbar/manage/Assignments/label" />
<hint ref="navbar/manage/Assignments/hint" />
</item>
</section>
</menu>
In this file, we specify an optional title for the navbar and each displayable element has an @ref attribute containing an XPath expression referring to localized content in an XML resource file. The "label" elements are typically displayed to the user while the "hint" elements are typically used for mouseover events . Section elements can have child sections.

The navbar is rendered by the following entry in the page-flow.xml file at the top-level of the Orbeon navigation tree:

<!-- navbar resources -->
<page id="navbar" path-info="/navbar" view="/nav.xpl"/>
And for the curious, the nav.xpl is as follows:
<p:param type="output" name="data" />

<p:processor name="oxf:pipeline">
<p:input name="config" href="/i18n/resources.xpl" />
<p:output name="data" id="resources" />
</p:processor>

<p:processor name="oxf:unsafe-xslt">
<p:input name="data" href="oxf:nav.xml" />
<p:input name="resources" href="#resources" />
<p:input name="config" href="nav.xsl" />
<p:output name="data" ref="data" />
</p:processor>
As one can see, the first processor gets the localized resource file from i18n/resources.xpl and this content serves as input to the XLST transformation generating the HTML code for the navbar.

The nav.xsl file is a bit long so I prefer not to paste a complete version here but if someone is really interested, I can make it available somehow. Nevertheless, the XSL does use a few tricks to get things done so I included part of the file here. The remaining code is just more of the same.
<xsl:import href="/oxf/xslt/utils/evaluate.xsl" />
<xsl:variable name="resources" select="doc('input:resources')/resources"
as="node()" />
<xsl:template match="/">
<div>
<xsl:if test="menu/title">
<h1>
<xsl:value-of
select="if (menu/title/@ref) then fn:evaluate($resources, menu/title/@ref, ()) else menu/title" />
</h1>
</xsl:if>
<ul class="tree-sections">
<xsl:apply-templates select="menu/section" />
</ul>
</div>
</xsl:template>
Essentially, we make use of the /oxf/xslt/utils/evaluate.xsl file that provides us with a fn:evaluate function (you do need to declare the following namespace: xmlns:fn="http://www.orbeon.com/xslt-function"). This function is used to evaluate the XPath expressions retrieved from the nav.xml file. In essence, we bind the $resources variable to the root node of the localized resources file and then use the following idiom each time we need a localized string:
   fn:evaluate($resources, [XPATH_EXPR]/@ref, ())
Where [XPATH_EXPR] is the (relative) path to the navbar element and @ref is the attribute containing the XPath expression for the String resource in the $resources document.

Finally, we need to modify the epilogue pipeline processor (epilogue-servlet.xpl) to force it to use our navbar processor. I added the following processor in the
<p:when test="/xhtml:html"> section of the epilogue-servlet.xpl:
<p:processor name="oxf:pipeline">
<p:input name="config" href="/nav.xpl" />
<p:output name="data" id="navbar" />
</p:processor>
And the following input to the
<p:when test="not(p:property('oxf.epilogue.use-theme') = false())">
section:
 <p:input name="navbar" href="#navbar"/>
This scheme finally works out really well. It could certainly be enhanced and made even more general but for the time being it serves me well and follows the KISS principle although it does make use of a few tricks.

Have a nice day

2 comments:

  1. Great solution. I've to redesign my orbeon application this way. Do you have any idea, how to add language change combobox (select1, xforms) in the application header div?

    ReplyDelete
  2. Sorry for a late response.

    I did implement a quick and not totally satisfactory solution to change the language associated with the session. It works however. I guess I would have to post a somewhat detailed description of it but here is a (very) short rundown:

    You can use the following pipeline:

    [p:processor name="oxf:scope-serializer"]
    [p:input name="config" href="scope-key.xml"/]
    [p:input name="data" ref="#instance" /]
    [/p:processor]

    [p:processor name="oxf:scope-generator"]
    [p:input name="config" href="scope-key.xml" /]
    [p:output name="data" ref="data" /]
    [/p:processor]

    In my case, the scope-key.xml looks like this:

    [config]
    [key]workforse/locale[/key]
    [scope]session[/scope]
    [session-scope]application[/session-scope]
    [/config]

    Hope this helps. Maybe more later.

    ReplyDelete