XRX/XSLTForms and eXist

< XRX

Motivation

edit

You want to port your standard XForms applications to work with XSLTForms and to be able to dynamically generate XForms from eXist XQueries.

Method

edit

XSLTForms is a very lightweight XForms processor that can be configured to run either on the web server or the web client. XSLTForms transforms each XForms element in the XForms application into HTML with some JavaScript. If you are using the client to do the transform, you must be aware of some limitations in client side-browsers.

Client-Side or Server-Side Transform: Design Tradeoffs

edit

The design decision of where to execute the transform, on the client or on the server, is one that may vary based on your situation. Transforming the form on the server can sometimes avoid the transmission of the full XSLT transform to the client and it also avoids browser-dependent variations. This process also may work more naturally for pipeline operations that use tools like XProc on the server.

On the other hand, once the first form is loaded, the XSLT transform can be read from the local browser cache. This allows you to only have to transmit the actual XForms application specification and not the much larger JavaScript and HTML code. So this option can minimize the network traffic as well as minimize the delay in displaying the changes.

The XSLTForms application is a library for doing XSL transforms based on the XForms input file. The output is a combination of HTML and JavaScript that will run across many browsers (Firefox, Internet Explorer 6, 7 and 8, Apple Safari, Google Chrome, and Opera).

Note that for many internal forms on an intranet, it is possible to request that all your users use a web browser that supports in-client plugins. This means that your XForms load times are very fast and your testing process is simple. For public forms, you often can not specify what browser the users must use, so the forms developer is required to test on many different versions of many different browsers. This can dramatically slow the forms development process and can cause testing and development costs to grow.

Disabling betterFORM on eXist 2.0 and 2.1

edit

eXist 2.0 and 2.1 come with the server-side XForms processor betterFORM installed as default. In order to allow XSLTForms to process XForms, you should do one of the following:

To use XLSTForms in a certain context, but allow the use of betterFORM in other contexts, set the following dummy attribute in your XQuery:

   let $attribute := request:set-attribute('betterform.filter.ignoreResponseBody', 'true')

in the XQuery that generates your form.

In order to disable betterFORM completely, change

<property name="filter.ignoreResponseBody" value="false"

to "true" in $EXIST_HOME/webapp/WEB-INF/betterform-config.xml

or comment out the entries for XFormsFilter in $EXIST_HOME/webapp/WEB-INF/betterform-config.xml and/or in $EXIST_HOME/webapp/WEB-INF/web.xml and restart.

Browser Portability Issues

edit

One of the large challenges attendant on supporting multiple versions of browsers from many vendors is that the way they render CSS can be dramatically different. Since XForms frequently leverage advanced CSS structures for form layout, this can be a large concern for XForms application developers. IE 6 can be especially troublesome due to its lack of full support for the CSS box model. This is especially true of forms that do not use HTML Table layouts to render forms. What was a simple style sheet for a single browser may often grow to be very complex and require many days of testing. For example the use of inline-block has many variations on many different browsers.

The best defense against the browser incompatibilities is to keep your forms very small and simple. Use many different class attributes and don't use repeating structures unless absolutely necessary.

Problem with Namespace Axis in Firefox 3.0

edit

There is currently a namespace bug in the Mozilla Firefox browser that prevents any instances from being loaded that use the non-null namespaces. For example the following XML file cannot be loaded into an instance using the src attribute.

<ex:root xmlns:ex="http://www.example.com">
    <ex:string>This is a test string.</ex:string>
</ex:root>

The workaround is to put a dummy namespace prefix reference in the HTML root element, such as:

<html
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:xf="http://www.w3.org/2002/xforms"
    xmlns:ex="http://www.example.com"
    ex:dummy="dummy" >
    <head>

The ex:dummy="dummy" will fix the problem and the instances will load correctly into the form. This patch may not work on other versions of Firefox. Note that the instance can be loaded inline to get around this bug. Also note that using the default namespace in the instance will also not work.

This is known as the Firefox Namespace Axis Problem. This is not a problem in Internet Explorer, Safari, Opera or Chrome. Firefox is the only browser that does not support namespace axis functions. XML support appears to be a low priority within Mozilla. We encourage anyone that feels that Firefox should support XML namespaces to vote for this bug fix on the Mozilla web site.

See Mozilla Bug ID 94270. The comment by C. M. Sperberg-McQueen is very informative.

The article by Uche Ogbuji discusses the new features Firefox 3.0 offers for XML processing.

How XSLTForms works

edit

To convert your standard XForms to work with XSLTForms you only have to add the following statement as the first line of your XForms file.

  <?xml-stylesheet type="text/xsl" href="/exist/rest/db/xforms/xsltforms/xsltforms.xsl"?>

The actual URL to use on your eXist installation may vary depending on your version. This document is large for an XSLT stylesheet (currently about 114KB and 4,500 lines long), but much smaller than many JavaScript libraries that run over 50,000 lines of code. The XSLT process also loads a 363KB JavaScript file. The key is that once it is loaded for the first form it can be stored in the web cache. A compressed version of this file may provide faster loading times in the future. This process will always be slower than a native XForms processor like the Firefox XForms extension, but the benefit is that it will work on multiple browsers once the XForms stylesheets have been tested on one browser.

Controlling Client and Server XSLT Execution

edit

By default, eXist will always attempt to perform server-side XSLT processing whenever it encounters an XML Processing Instruction that signals that it should perform a transform.

If your XForms are static, meaning that they do not change at all based on the context of the situation, you can store the forms anywhere on a web file system. They do not need to be stored in eXist.

If you store the XForms in eXist, there are two items you must be aware of.

By default, eXist processes all XML XSL processing instructions and runs them on the server BEFORE they ever reach the browser. This is not the way that XSLTForms was originally designed to work. It also requires a lot more bandwidth between the client and the server.

To disable server-side XSLT processing you will need to add the following instruction to your XQuery that generates the XForms:

  declare option exist:serialize "method=xhtml media-type=text/xml";
  declare option exist:serialize "indent=no";
  declare option exist:serialize "process-xsl-pi=no";

The first line sets the XHTML serialization type and also sets the standard XML mime type.

Note: Many browsers such as IE do not recognize the proper XForms mime type of application/xhtml+xml. To get around this problem you can use the incorrect mime type of text/xml. There are ways to fix IE to recognize the correct mime types by making changes to the Windows registry but these changes are difficult to make for general users. For more details see here

The second line is required by the current release of XSLTForms. The last line tells the server-side to not process the transform.

These three options can be combined into a single statement:

  declare option exist:serialize "method=xhtml media-type=text/xml indent=no process-xsl-pi=no";

Note that in the context of eXist, process-xsl-pi=no means "do not process this on the server before you send it to the browser".

One approach to generating dynamic forms is to use the following style:

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>
      <title>XForms Template</title>
      <xf:model>
         <xf:instance xmlns="" id="save-data">
            <data/>
        </xf:instance>
      </xf:model>
   </head>
   <body>
      <h1>XForms Template</h1>
      <p>This XForms application has been dynamically generated from an XQuery</p>
   </body>
</html>

You can then return the following at the end of your form:

  let $xslt-pi := processing-instruction xml-stylesheet {'type="text/xsl" href="/exist/rest/db/xforms/xsltforms/xsltforms.xsl"'}
  return ($xslt-pi,$form)

For debugging you can also use:

  let $xslt-pi := processing-instruction xml-stylesheet {'type="text/xsl" href="/exist/rest/db/xforms/xsltforms/xsltforms.xsl"'}
  let $debug := processing-instruction xsltforms-options {'debug="yes"'}
  return ($xslt-pi, $debug, $form)


Here the href points to the location you have installed your XSLTForms library.

Server-side Transformation using the transform Function

edit

eXist provides a method for doing a server-side XSLT transform using the transform module. The following is an example of using this transform function to perform server-side transformation of the form.

xquery version "1.0";
declare option exist:serialize "method=xhtml media-type=text/html indent=no";

let $form :=
<html xmlns="http://www.w3.org/1999/xhtml" 
   xmlns:ev="http://www.w3.org/2001/xml-events" 
   xmlns:xf="http://www.w3.org/2002/xforms">
    <head>
      <title>XForms Template</title>
      <xf:model>
         <xf:instance xmlns="" id="save-data">
            <data>
               <name>John Smith</name>
            </data>
        </xf:instance>
      </xf:model>
   </head>
   <body>
      <h1>XForms Test Program</h1>
      <xf:input ref="name">
         <xf:label>Name: </xf:label>
      </xf:input>
   </body>
</html>

let $transform := '/exist/rest/db/xforms/xsltforms/xsltforms.xsl'

let $params := 
<parameters>
   <param name="omit-xml-declaration" value="yes"/>
   <param name="indent" value="no"/>
   <param name="media-type" value="text/html"/>
   <param name="method" value="xhtml"/>
   <param name="baseuri" value="/exist/rest/db/xforms/xsltforms/"/> 
</parameters>

let $serialization-options := 'method=xml media-type=text/html omit-xml-declaration=yes indent=no'

return 
   transform:transform($form, $transform, $params, $serialization-options)

Using XSLTForms on the Server Side in a URL Rewrite Filter

edit

It is also possible to perform server-side transformation using eXist's URL Rewrite function.


Sample from /eXist/webapp/xforms/controller.xql

<dispatch xmlns="http://exist.sourceforge.net/NS/exist">
   <view>
      <forward servlet="XSLTServlet">
         (: Apply xsltforms.xsl stylesheet :)
         <set-attribute name="xslt.stylesheet" value="xsltforms/xsltforms.xsl"/>
         <set-attribute name="xslt.output.omit-xml-declaration" value="yes"/>
         <set-attribute name="xslt.output.indent" value="no"/>
         <set-attribute name="xslt.output.media-type" value="text/html"/>
         <set-attribute name="xslt.output.method" value="xhtml"/>
         <set-attribute name="xslt.baseuri" value="xsltforms/"/>
      </forward>
   </view>
   <cache-control cache="yes"/>
</dispatch>

See Example from eXist subversion

Modifying your CSS to work with XSLTForms

edit

Because XSLTForms uses XSLT on the web client, it only knows how to parse XML files. To get your CSS files to work you will have to make each of your CSS files a well-formed XML document. This can be easily done by simply wrapping your CSS file with a root data element such as:

<css>
   /* empty rule to recover from parse errors */
   empty {}
   body {font-family: Arial, Helvetica, sans-serif;}
   label {font-weight: bold;}
</css>

If you wish your CSS to be both valid CSS and well-formed XML, you can use a special prolog <css><![CDATA[/**/ and epilog /*]]>*/</css> as shown in the two examples below. As interpreted by an XML parser:

<!--/*--><css><![CDATA[/**/

h1 {
   body {font-family: Arial, Helvetica, sans-serif;}
   label {font-weight: bold;}
}

/*]]>*/<!--/*--></css><!--*/-->

The same file interpreted by a CSS parser:

<!--/*--><css><![CDATA[/**/

h1 {
   body {font-family: Arial, Helvetica, sans-serif;}
   label {font-weight: bold;}
}

/*]]>*/<!--/*--></css><!--*/-->

You can then just import the CSS file into your XForms file using the link element:

  <link type="text/css" rel="stylesheet" href="style-with-root-wrapper.css"/>

XQuery can also be used to automatically generate the CSS file from the server. On the server the CSS file can be wrapped, but for non XSLTForms clients the root element can be removed.

If there are small changes in the CSS that are not global to all other forms, you can insert them directly into the <style> tag of the form. Note that if your XForms are dynamically generated from an XQuery you must enclose the style with CDATA wrappers or use double curly braces {{ and }} to escape the XQuery processing.

<style type="text/css">
    <![CDATA[
       @namespace xf url("http://www.w3.org/2002/xforms");
          
       .block-form xf|label {
          width: 15ex;
       }
          
       .PersonGivenName .xforms-value {width:20ex}
       .PersonSurName .xforms-value {width:25ex}
   ]]>
</style>

Note that in the Firefox plugin the CSS class for changing the value was .xf-value. In XSLTForms the class is .xforms-value.

Disabling the CSS Conversion Option

edit

XSLTForms also allows you to add the following XML Processing Instruction flag to your XForms file.

  <?css-conversion no?>
  <?xsltforms-options debug="no"?>

If you add these lines the CSS files will not need to be converted to XML files. You can not use the @namespace xf features within the CSS, and you may have to add class attributes to tags, but some users prefer this method.

Formatting XForms Elements

edit

The following syntax should be used for styling XForms elements:

 @namespace xf url("http://www.w3.org/2002/xforms");
 
 xf|label {font-weight: bold;}

Note that this format has been tested with XSLTForms on IE 8, Firefox, Opera and Safari running on Windows, the Mac and Linux.

Rendering Block Forms

edit

Block forms are forms that use a block layout for each control so each control appears on a different line. Block forms have a consistent layout and have room for hint text, required field labels and help icons. Inline forms that have multiple fields on a line should be used only when you have screen area constraints.

 
Example of a block form

To render block forms correctly we suggest you wrap all block forms in a standard div tag with a class of "block-form":

   <div class="block-form">
      ...put the controls here...
  </div>

This allows you to use a CSS file where the block display items are only triggered if they are decendants of the block form div element.

Here are some sample CSS rules for block forms.

/* formatting rule for all XForms controls elements within a block form.  Each input control is on a separate line */
.block-form xf|input,
.block-form xf|select,
.block-form xf|select1,
.block-form xf|textarea {
     display: block;
     margin: 1ex;
}

/* formatting rule for all labels in a block form */
.block-form xf|label {
    display: inline-block;
    width: 15ex; /* fix the width of all the labels in the block form */
    float: left;
    text-align: right;
    margin-right: 1ex; /* margin to the right of the label */
}

The most risky part of this is the inline-block rule. This is required to allow you to fix the width of the label.

Aligning select1 with appearance=full

edit

If you want to display all the items in a selection list you can add the attribute

  <xf:select1 appearance="full">

To keep the item values together you must add the following to your CSS file:

 xf|select1 xf|item {
   margin-left: 21ex;
 }

Using xf:load in place of xf:submission

edit

The current release of XSLTForms does not support the xf:submission element with the correct URL rewrite when the replace="all" attribute is used. This structure is used in many search forms, when a form is used to gather a set of parameters, such as keywords and date ranges that are used to limit the search. These search parameters are traditionally stored in a single instance in the model and used to construct a URL for a restful search service.

Here is an example of search parameters stored in an instance in your model:

<xf:instance xmlns="" id="search-params">
    <data>
       <url>http://www.example.com/search.xq?q=</url>
       <q></q>
    </data>
</xf:instance>

XSLTForms does however support the xf:load function, and when you use xf:load with the xf:resource subelement you can achieve the same result.

For example within a trigger action you can use the following:

<xf:trigger>
   <xf:label>Search</xf:label>
   <xf:action ev:event="DOMActivate">
      <xf:load show="replace">
         <xf:resource value="concat( 'search.xq?q=', instance('search-params')/q )"/>
      </xf:load>
   </xf:action>
</xf:trigger>

Controlling Namespace Prefixes in the Output

edit

XSLTForms can change the way that namespace prefixes in your instance data are managed. If you load an instance with a default namespace, all the submitted data will include a namespace prefix.

If your input is:

<task xmlns="http://www.example.com/task">
    <id/>
    <task-name/>
    <task-description/>
</task>

Then the saved output will become:

<task:task xmlns="http://www.example.com/task">
    <task:id/>
    <task:task-name/>
    <task:task-description/>
</task:task>

This can be controlled by the includenamespaceprefixes attribute of the submission element.

Bind Restrictions

edit

With XSLTForms you can only have a single bind per element in your instance. So for example you can not put the data type in a separate bind as a required bind. It will generate an error, but will not indicate which element is bound twice.

Set Focus on Inserts

edit

There is no way to set the focus on a new insert.

See Repeat Test 1

Upload file with XSLTForms

edit

Versions of XSLTForms earlier than rev. 537 (April 2012) do not support the XForms Control upload.

For versions 537 and later, the XForms "upload" control is supported and behaves essentially as described in the XForms 1.1 specification.

For version earlier than 537, there is a work-around.

In case of submission method "xml-urlencoded-post" XSLTForms dynamically build a form whose ID is "xsltforms_form"

If this form still exists, XSLTForms replaces the value of the first child of the form by the serialization of the submitted instance content.

Example

Model

  <xf:model> 
   <xf:instance> 
    <data> 
     <comment>Upload one or two files</comment> 
    <data> 
   </xf:instance> 
   <xf:submission id="sub" method="xml-urlencoded-post" replace="all" action="load.xql"> 
    <xf:message level="modeless" ev:event="xforms-submit-error"> Submit error. </xf:message> 
   </xf:submission> 
  </xf:model>

Form

  <form name="upload" id="xsltforms_form" action="load.xql" enctype="multipart/form-data" method="post"> 
   <input type="hidden" name="postdata"/> 
   <input type="file" name="file1" style="width: 360px" />   
   <input type="file" name="file2" style="width: 360px" />     
  </form>

Submission

  <xf:submit submission="sub"> 
   <xf:label class="label">Send</xf:label> 
  </xf:submit>

XQuery load.xql

...
let $collection := "/db/my/docs"
let $f1 := request:get-uploaded-file-name("file1")
let $f2 := request:get-uploaded-file-name("file2")
let $data := request:get-parameter("postdata",())
let $o1 := if ($f1 ne "") then xdb:store($collection, xdb:encode-uri($f1), request:get-uploaded-file-data("file1")) else ()
let $o2 := if ($f2 ne "") then xdb:store($collection, xdb:encode-uri($f2), request:get-uploaded-file-data("file2")) else ()
...

This is a trick that enables uploading files within an XSLTForms application. File content is not loaded in instance.

References

edit

Acknowledgements

edit

Most of the above materials were generated with the kind assistance of the XSLTForms author Alain Couthures.


Back: Patching your Browser to Support XForms Next: Configuration File Editor