In the previous article, I showed you how to manage user input in the XForms model. I discussed the process of submitting an XForm and - more importantly - validating user input prior to submission using built-in XML Schema support.
In this concluding segment, I'll introduce you to some of XForms' more arcane features. First, the concept of binding, which allows developers to implement a number of useful features quickly - calculating values instantly, enforcing basic validation rules, taking decisions on the fly or rendering a particular node non-writable.
Next, I'll briefly show you how you can use XPath functions within an XForms model definition, and illustrate how functions like sum() and avg() allow you to perform complex calculations in a simple and smooth manner. I'll also take a quick look at the XForms event processing model, explaining the important phases and showing you a basic example of how triggers and actions work.
Let's get started, shall we?
Operating With Extreme Caution
You've already seen how various XML technologies integrate seamlessly with each other when it comes to XForms. In fact, in the last part of this tutorial, I showed you how to re-use an XML Schema datatype definition in an XForms model, and how to use XPath expressions to reference and bind instance data with the XForms user interface. But why stop there? You can also use logical, comparison and arithmetic operators and functions with XPath expressions, in order to perform calculations using the instance data in an XForms model.
In order to do this, you need to first understand the
<xforms:bind> element, which makes it possible to bind instance data elements to specific properties and constraints. The
<xforms:bind> element comes with a number of additional attributes, which can be used to specify whether a particular element of the instance data is required, read-only, constrained to specific values or compliant with a specific type.
Consider the following example, which demonstrates:
<!-- form model --> <xforms:model id="account"> <xforms:instance> <account> <name /> <number /> </account> </xforms:instance> <xforms:bind id="accountNumberRequired" nodeset="/account/number" required="true()" /> </xforms:model>
In this case, the bind definition states that the account number is required in order for the form to be submitted. This is a very fundamental example of input validation. With normal HTML forms, you'd need to write client-side and server-side code to manage this requirement; with XForms, it comes built-in!
In case you were wondering where this bind definition gets used, the XForms specification states that user interface elements can then be linked to the definition, simply by adding a "bind" attribute to the interface control (instead of the traditional "ref" attribute). Here's an example:
<!-- define the form interface --> <xforms:input bind="accountNumberRequired"> <xforms:label>Account number</xforms:label> </xforms:input>
Money, Money, Money
Of course, the "required" attribute is just one example of what XForms calls "model item properties". Another one is the "relevant" attribute, which specifies when a particular element of the instance data is enabled or disabled. Consider the following example, which illustrates:
<!-- form model --> <xforms:model id="tran"> <xforms:instance> <transaction> <type /> <amount /> <checkNumber /> </transaction> </xforms:instance> <xforms:bind nodeset="/transaction/checkNumber" relevant="/transaction/type='check'" /> </xforms:model>
Basically, this says that when performing a transaction (in this case, a bank account transaction which is either a deposit or a withdrawal) the check number is only relevant when the transaction involves a check.
Of course, it's up to the implementation to decide how to handle this particular property. Some implementations might disable the field for data entry during cash transactions, others might hide it, and still others might pop up a warning.
You'll notice, also, that the "relevant" property contains an equality test. Model item properties can contain comparison tests, so long as these conform to the rules laid down for XPath expressions. Here's another example, this one illustrating the use of a comparison test with the previously-explained "required" property by requiring the entry of a tax identification number for transactions greater than $50,000:
<!-- form model --> <xforms:model id="taxes"> <xforms:instance> <transaction> <name /> <amount /> <taxID /> </transaction> </xforms:instance> <xforms:bind id="taxIDRequired" nodeset="/transaction/taxID" required="/transaction/amount > 50000" /> </xforms:model>
Shop Till You Drop
It's also possible to specify that a particular element of the instance data be read-only via the "readonly" property. Consider the following example:
<!-- form model --> <xforms:model id="shop"> <xforms:instance> <item> <code /> <price /> <discount>2.00</discount> <quantity /> <grossTotal /> <netTotal /> </item> </xforms:instance> <xforms:bind nodeset="/item/discount" readonly="true()" /> </xforms:model>
In this case, since you don't want users messing with the discount rate, you can use the "readonly" property to make it non-modifiable. As before, you can combine this with comparison tests to make the property's relevance conditional.
One extremely useful property in this context - and one you'll probably be using a lot - is the "calculate" property, which lets you calculate some of the form data on the fly. Consider the following extension of the example above, which illustrates:
<!-- form model --> <xforms:model id="shop"> <xforms:instance> <item> <code /> <price /> <discount>2.00</discount> <quantity /> <grossTotal /> <netTotal /> </item> </xforms:instance> <xforms:bind nodeset="/item/discount" readonly="true()" /> <xforms:bind nodeset="/item/grossTotal" calculate="/item/price * /item/quantity" /> <xforms:bind nodeset="/item/netTotal" calculate="/item/grossTotal - ((item/grossTotal * /item/discount)/100)" /> </xforms:model>
In this case, the "calculate" property has been used to dynamically calculate the value of the
<grossTotal> element from the values of the
<quantity> elements. The
<netTotal> is then again calculated from the
<grossTotal> by accounting for the
In case you were wondering why I haven't used a "readonly" property for the two dynamically-calculated values - I don't need to, since these values are automatically made read-only when a "calculate" property is used on them.
The Bookworm Turns
In addition to simple comparison tests, you can also use XPath arithmetic and non-arithmetic functions in an XForms model. Consider the following example, which illustrates:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xforms="http://www.w3.org/2002/xforms/cr" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <head> <!-- define the form model --> <xforms:model id="bookstore"> <xforms:instance> <inventory> <books> <title>Be Cool</title> <author>Elmore Leonard</author> <price>7.97</price> <quantity>150</quantity> <title>Mystic River</title> <author>Dennis Lehane</author> <price>25.00</price> <quantity>86</quantity> <title>Hit List</title> <author>Lawrence Block</author> <price>23.76</price> <quantity>26</quantity> <title>Silent Joe</title> <author>T. Jefferson Parker</author> <price>24.99</price> <quantity>268</quantity> <title>The Travel Detective</title> <author>Peter Greenberg</author> <price>34.87</price> <quantity>9</quantity> </books> <totalInventory /> <mostExpensive /> <leastExpensive /> <averageCost /> </inventory> </xforms:instance> <xforms:bind nodeset="/inventory/books/totalInventory" calculate="sum(/inventory/books/quantity)" /> <xforms:bind nodeset="/inventory/books/mostExpensive" calculate="max(/inventory/books/price)" /> <xforms:bind nodeset="/inventory/books/leastExpensive" calculate="min(/inventory/books/price)" /> <xforms:bind nodeset="/inventory/books/averagePrice" calculate="avg(/inventory/books/price)" /> </xforms:model> </head> <body> <!-- define the form interface --> <!-- loop over the instance data --> <xforms:repeat nodeset="/inventory/books"> <xforms:input id="txttitle" ref="title"> <xforms:label>Title</xforms:label> </xforms:input> <xforms:input id="txtauthor" ref="author"> <xforms:label>Author</xforms:label> </xforms:input> <xforms:input id="txtprice" ref="price"> <xforms:label>Price</xforms:label> </xforms:input> <xforms:input id="txtquantity" ref="quantity"> <xforms:label>Quantity</xforms:label> </xforms:input> </xforms:repeat> <!-- print calculated values --> Total inventory: <xforms:output ref="/inventory/books/totalInventory" /> <br /> Price of most expensive book: <xforms:output ref="/inventory/books/mostExpensive" /> <br /> Price of least expensive book: <xforms:output ref="/inventory/books/leastExpensive" /> <br /> Average price: <xforms:output ref="/inventory/books/averagePrice" /> </body> </html>
As usual, all the action is centered around the definition of the XForms model. This time around, the instance data has been populated with the inventory of the neighbourhood bookstore, and XPath functions like sum(), min(), max() and avg() have been used to perform calculations on that data, and assign the results of those calculations to specific summary elements in the instance data. This data is then displayed in the form using the
You'll also notice a new element in the form above - the
<xforms:repeat> element. This element gives XForms authors the ability to loop over a collection of nodes without having to resort to complex "while" or "for" loops. In this example, the
<xforms:repeat> element causes the XForms processor to iterate over the
<books> collection from the instance data, and display the values of the title, author, price and quantity. The "nodeset" attribute specifies the node over which iteration should take place, while the loop counter is handled internally by the processor. Looping will stop once no further match is found for the "nodeset" criteria.
Why stop just at numeric functions, though? This next example uses the days-from-date() function to calculate the number of days between an entered date and January 01 1970.
The XForms model will look something like this:
<!-- form model --> <xforms:model id="dateCalculator"> <xforms:instance> <calc> <dt /> <numDays /> </calc> </xforms:instance> <xforms:bind nodeset="/calc/numDays" calculate="days-from-date(/calc/dt)" /> </xforms:model>
Pretty straightforward, isn't it?
An Event To Remember
The XForms 1.0 specification divides the process of event handling into four phases:
Initialization: This is the very first stage, wherein the XForms processor "wakes up" and begins construction of the data model using the instance data provided. This is also the stage where all relevant XML Schemas are loaded, and all form controls (with their associated bindings) are initialized.
Interaction: Interaction events are fired as a result of action from the user - for example, keyboard navigation to a new input control, mouse clicks, data entry or item selection. A number of different events can occur in this phase, and each one has a default action associated with it.
Notification: Notification events don't usually have a default action associated with them; rather, they're triggered as a result of a change in the form state, such as a form control receiving focus or a button being clicked.
Error handling: These events occur due to errors in XForm processing, such as illegal binding expressions or illegal XPath references. Errors may be either fatal or non-fatal, depending on their severity.
OK, now enough of the theory. Let's look at a simple example:
<!-- define the form model --> <xforms:model id="bankAccount"> <xforms:instance> <account> <openingBalance>10000</openingBalance> <withdrawal /> <closingBalance /> </account> </xforms:instance> <xforms:bind id="nodeClosingBalance" nodeset="/account/closingBalance" /> </xforms:model> <!-- define the form interface --> <xforms:input id="txtwithdraw" ref="/account/withdrawal"> <xforms:label>Amount to withdraw</xforms:label> <xforms:hint>Please enter the amount you wish to withdraw</xforms:hint> </xforms:input> <xforms:trigger> <xforms:label>Calculate Closing Balance</xforms:label> <xforms:setvalue ev:event="DOMActivate" bind="nodeClosingBalance" value="/account/openingBalance - /account/withdrawal" /> </xforms:trigger>
By definition, the
<xforms:trigger> element is the XForms counterpart of the regular HTML form button. When clicked, it generates a DOMActivate event, which is a catch-all event type fired whenever any event (pressing a button, selecting an option) occurs. The
<xforms:setvalue> element, which is used to set the value for a particular node after computing it from an XPath expression, listens for this event and acts when it receives it. In this case, the action involves subtracting the withdrawal amount from the opening balance to obtain a new closing balance.
A number of other event types are available in the XForms model - you've already seen one of the other important ones, the xforms-submit event, in the previous segment of this tutorial. There are too many to list here, so you should take a look at the specification (and the excellent examples included within it), to better understand this topic.
And that's about all I have for you. Over the course of this three-part tutorial, I introduced you to the new XForms 1.0 specification, and showed you how to create a data model and bind its elements to form input controls. I also explained the various XForms input controls, and showed you how they map into the standard HTML form controls.
Next, I discussed the process of submitting an XForm, with a look at the
<xforms:submission> element and the "submit" input control. I also showed you how to perform primitive datatype validation in an XForm using built-in XML Schema datatypes, and how to extend this to perform more complex validation by integrating your own custom datatypes into the XForms model.
Finally, in this concluding part, I introducing the concept of binding using the
I hope you enjoyed this article, and that it gave you sufficient grounding to get you started with XForms. XForms is one of the more interesting emerging XML technologies, and you can expect to see it grow more powerful in subsequent iterations. So go on - it's time to practice!
Note: Examples are illustrative only, and are not meant for a production environment. Melonfire provides no warranties or support for the source code described in this article. YMMV!This article was first published on 17 Nov 2003.