All XForm sites

A few months ago I was looking at an XForm-based menu for a project. Since then I’ve been thinking about the idea of creating a complete site using nothing but the xform system. It isn’t as daft as it might sound at first glance, many sites are basically an initial page with subsequent content AJAX’d into them via something like JQuery. I’ve never been very comfortable with this design and the disjoint between content and the code intercepting events: and not being a fan of JS, perhaps I’m more motivated.

The Simple version

So, in the XForms world, how does this work? The simplest way is to have two instances     in the form: a) One for the menu, b) One for the body data.

 
<xf:instance id="bodydata" xmlns="">
<data>
<title>PlaceHolder</title>
<content>This is the placeholder content</content>
</data>
</xf:instance>

<xf:instance id="navdata" xmlns="">
<data>
<item><title>Home</title><id>1</id></item>
<item><title>Journal</title><id>2</id></item>
<!-- Other menu items -->
</data>
</xf:instance>

<xf:submission id="get-data" method="get" replace="instance" 
instance="bodydata" 
ref="instance('navdata')/item[index('nav-repeat')]/id" 
action="query.xql"/>

Now we can construct a basic site where where the current content of the data instance is <xf:output> into the html. I’m using the mime-type “xhtml+xml” so that it flows with the rest.
The navigation is an <xf:repeat> of <xf:submission> items styled as links using ‘minimal’ appearance. These call a submission event that pulls html into the bodydata instance. Note that since I’ve only got one navigator to call the submission from, I can put the choice logic into that submission, rather than have a separate node with the current value in and having to use the slightly more wordy <xf:trigger> construct.

<div id="navigation">
<ul>
    <xf:repeat nodeset="instance('navdata')/item" id="nav-repeat">
        <li><xf:submit submission="get-data" appearance="minimal">
            <xf:label><xf:output ref="title"/></xf:label></xf:submit>
        </li>
    </xf:repeat>
</ul>
</div>

<!-- The content -->
<div id="content">
<h3><xf:output  ref="title"/></h3>
<xf:output ref="content" mediatype="application/xhtml+xml"/>
</div>

Now, there are obvious issues with this sort of approach:

  • We can’t easily put references to other content in the data. That’s because they wouldn’t be part of the form, just simple links.
  • If we only want to pull in documents, that’s ok. But what if we wanted lists, searches, editing etc, that simple instance is going to get very complicated, or we’re going to end up with a sea of <xf:switch> statements and a load of redundancy.

Luckily, there is at least a partial solution: the Sub-form.

THE SubForm version

Sub-forms allow you to load a complete form into another form. The mechanism is as far as I know due to be in XForms 2.0, but there are several engines that support it in some way already, such as XSLTForms and BetterForm. The basic mechanism is that instead of a submission, you use <xf:load> with the ’embed’ directive and the id of an <xf:group> in the parent. This gets replaced by the subform. Now what is cool is that the sub-form can either interact with the parent model or bring in a model of it’s own with it’s own instances and use that instead (as can the parent).

Armed with the sub-form idea, we can look at our site again and bring in a bit of MVC, i.e the idea of a view. So, imagine that we have a List sub, a Search sub, a View sub, a Edit sub etc, all forms in their own right, pulled into the parent form as needed. For instance we could use a search field in the parent to trigger a <xf:load> to pull in the “Search” sub form. This could have not only the results in it’s own model/instance, but also it’s own submission buttons for <next> <prev>. That’s a great modular approach and of course each sub-form can be tested on it’s own.

So, now we can alter our little site.  We don’t need our data instance now, but we need a destination for the sub-form instead:

<!-- The content -->
<div id="content">
    <xf:group id="subform/>
</div>

We need to change the navigation, so we’ll set up a ‘view’ sub-form instead and some way to be able to send it parameters (the document we want) in the <xf:load>.
First step is replace our <xf:submit> items in the menu with the following:

<xf:trigger appearance="minimal">
<xf:label><xf:output ref="title"/></xf:label>
<xf:action ev:event="DOMActivate">
<xf:load show="embed" targetid="subform">
<xf:resource value="concat('view_doc.xql','?id=',id)"/>
</xf:load>
</xf:action>
</xf:trigger>
Notice that I’ve had to use <xf:resource> as well so I can calculate what id I need as you can’t do that in an <xf:load> “ref” field via concat() as you might imagine.
The XQuery-based subform, view_doc.xql is pretty simple. I’ve used a private instance to hold the data (in some ways this is very much like using sub-routines in a programming language with global and local variables) and hence I’ve had to use another model. Note that I then need to say what model I’m getting my data from in the subsequent <xf:output> or it will try and find it in the parent.
xquery version "1.0";
declare option exist:serialize "method=xhtml media-type=text/xml indent=no process-xsl-pi=no"; 

let $data_collection := '/db/projects/project1/'
let $q := request:get-parameter('id', '1')
let $file := concat($data_collection,$q,'.xml')

let $error :=
<document><title>Oops!</title>
<content><p>I can't find {$file} in the database</p></content>
</document>
let $input := if(doc-available($file) = true()) then (doc($file)) else $error
let $form :=
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xf="http://www.w3.org/2002/xforms">
<head>
<xf:model id="sub">
<xf:instance>{$input}</xf:instance>
</xf:model>
</head>
<body>
<h3 class="post-title"><xf:output model="sub" ref="title"/></h3>
<xf:output model="sub" ref="content" mediatype="application/xhtml+xml"/>
</body>
</html>
let $xslt-pi := processing-instruction xml-stylesheet {'type="text/xsl" href="/resources/xsltforms/xsltforms.xsl"'}
let $css := processing-instruction css-conversion {'no'}
let $debug := processing-instruction xsltforms-options {'debug="no"'}
return ($xslt-pi,$css,$debug,$form)
It might seem a bit verbose to do this for every view, but of course in the real world I’d template it or use XSLT to do the repetitive bits.
Lastly, we need some way to load initial data into our site. Since we now have a sub-form for showing the data, it makes sense to use that; all we have to do is put a suitable event in the model.
<xf:action ev:event="xforms-ready">
<xf:load show="embed" targetid="subform" resource="view_doc.xql"/>
</xf:action>

Thoughts and Next Steps

It’s possible to do a site as a complete XForm. Using sub-forms makes it feasible, though performance might be an issue and there are issues still to resolve, such as linking in the data. It might turn out that this is simply a good way to design a site and how it behaves.

Of course at the end of the day, in a browser, it becomes the slew of html and js I’ve been avoiding, but at least I didn’t have to write it. So, lets be honest, this isn’t a mainstream technique, but I quite like it. So, I’ll continue my experiments, create more views for listing and editing etc and see how the end result turns out.

Advertisements