 | Level: Intermediate Nicholas Chase (ibmquestions@nicholaschase.com), Freelance writer, Backstop Media
26 Jun 2007 If you've done any work in XML, you're probably familiar with XPath, the expression language that enables you to select portions of an XML document. If you've worked with XForms, you're definitely familiar with it; you can't build an XForms form without it! But XPath enables you to do much more than just select a node for display on the page. This article shows you how XPath and XForms interact to enable you to create functionality you may not have considered, such as displaying a list of unique values in one easy step, or using XPath in conjunction with interface elements such as radio buttons or drop-down lists to control the data displayed, as opposed to just the data submitted. This article assumes that you are familiar with XML, XForms, and the basics of XPath. If you need a refresher, you can find links in the Resources to get you started.
What you're going to accomplish
In the course of this article, you're going to create a report-editing form that makes use of some of the cool things you can do with XPath. The form shows you how to:
- Automatically populate a node using the results of an XPath function
- Create a master-detail form using radio buttons
- Display only unique items from a list
- Filter results based on multiple criteria
- Make XPath filter criteria optional
The form looks something like Figure 1.
Figure 1. The final form
You'll start by creating the basic form.
The basic form
Start by creating a form that includes the ability to edit a report's title and body (see Listing 1).
Listing 1. The basic form
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/D/tdxhtml1-strict.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
>
<head>
<title>Report Entry Editor</title>
<link rel="stylesheet" href="style.css" type="text/css"/>
<xforms:model >
<xforms:instance id="editor" >
<entry xmlns="">
<id />
<status />
<title />
<body />
</entry>
</xforms:instance>
<xforms:bind nodeset="instance('editor')/title" id="titlebind"/>
<xforms:bind nodeset="instance('editor')/body" id="bodybind"/>
<!-- Dummy submission; we're just dealing with the client -->
<xforms:submission id="editsubmission" action="savechanges.php"
method="post"/>
</xforms:model>
</head>
<body>
<h2>Editor</h2>
<xforms:input bind="titlebind" >
<xforms:label><b>Title: </b></xforms:label>
</xforms:input><br/>
<xforms:textarea bind="bodybind">
<xforms:label><b>Body: </b></xforms:label>
</xforms:textarea><br />
<xforms:submit submission="editsubmission">
<xforms:label>Save changes</xforms:label>
</xforms:submit>
</body>
</html>
|
This is just a basic form, but it has the ingredients you'll need for an editor. You can add a new report title and body, and then submit it, though this article doesn't deal with the submission. The result looks like Figure 2.
Figure 2. The basic form
The code also includes bindings for the elements so you can refer to them directly. In this case, the XPath statements are fairly simple and straightforward, but you can use bindings to create "shortcuts" so you don't have to work with unwieldy XPath statements directly in the code; the bind essentially creates a "nickname" for each XPath expression.
The next step is to create the entry ID field.
Automatically populating fields with XPath expressions
The entry ID attribute can be used to identify it, so you'll need a QName-ish value. In other words, you need a string with no spaces, punctuation, etc. To do that, you can randomly generate something on the server side, or you can create a value based on the data the user enters (see Listing 2).
Listing 2. Adding content automatically
...
<body />
</entry>
</xforms:instance>
<xforms:bind id="fixid" nodeset="instance('editor')/id"
calculate="translate(../title,'ABCDEFGHIJKLMNOPQRSTUVWXYZ ?!',
'abcdefghijklmnopqrstuvwxyz_') "/>
<xforms:bind nodeset="instance('editor')/id" id="sidbind"/>
<xforms:bind nodeset="instance('editor')/title" id="titlebind"/>
<xforms:bind nodeset="instance('editor')/body" id="bodybind"/>
...
<h2>Editor</h2>
<b>Entry ID:</b> <xforms:output bind="sidbind"/><br />
<xforms:input incremental="true" bind="titlebind" >
<xforms:label><b>Title: </b></xforms:label>
</xforms:input><br/>
...
|
In this case, you've created a binding that not only identifies a particular node, but also sets a value for it. The value, in this case, is the text entered in the title element, with all of the capital letters replaced with lowercase, spaces replaced with underscores, and question marks and exclamation marks removed entirely. Because you've now marked the title input field as incremental="true", this field updates as you type each key, as you can see in Figure 3.
Figure 3. Incrementally updating a field
In this case, you've used the XPath translate() function. Not only can you use all of the XPath 1.0-defined functions, you can also use functions specially created for XForms, such as now(). The XForms Recommendation includes:
-
if()
-
avg()
-
min()
-
max()
-
count-non-empty()
-
index()
-
property()
-
now()
-
days-from-date()
-
seconds-from-dateTime()
-
seconds()
-
months()
You can use any of these functions to automatically populate an instance node.
Creating data, however, is only part of what XForms gives you. You also have options for manipulating existing data.
Creating a master-detail form using a radio button
One handy trick you can use XPath for is creating a "master-detail" form. The idea is to capture the identity of a radio button the user clicked and use that to create an XPath statement that populates the editor with the appropriate data. To see that in action, add a few existing entries to a new instance (see Listing 3).
Listing 3. Working with existing data
...
<body />
</entry>
</xforms:instance>
<xforms:instance id="ui" xmlns="">
<ui>
<currentItem></currentItem>
<statusChoice> </statusChoice>
<authorId> </authorId>
<category> </category>
</ui>
</xforms:instance>
<xforms:instance id="entries" >
<entries>
<entry xmlns="" id="something_is_broken">
<status>1</status>
<category>General</category>
<author id="Nick">Nick Chase</author>
<title>Something is broken</title>
<body>I hear a lot of rattling around in the boxes when
I shake them up.</body>
</entry>
<entry xmlns="" id="glass_collection_broken">
<status>1</status>
<category>Specific</category>
<author id="Sarah">Sarah Chase</author>
<title>Glass collection broken</title>
<body>Who broke my glass collection? I had it packed in
a box but it looks like somebody just shook the
heck out of it.</body>
</entry>
<entry xmlns="" id="who_am_i">
<status>0</status>
<category>NonSensical</category>
<author id="Nick">Nick Chase</author>
<title>Who am I?</title>
<body>I'm sure there's some reason I'm here, but I just
... can't ... remember...</body>
</entry>
</entries>
</xforms:instance>
<xforms:bind id="fixid" nodeset="instance('editor')/id"
calculate="translate(../title,'ABCDEFGHIJKLMNOPQRSTUVWXYZ ?!',
'abcdefghijklmnopqrstuvwxyz_') "/>
...
<body>
<h2>Existing entries</h2>
<xforms:select1 incremental="true" appearance="full"
ref="instance('ui')/currentItem">
<xforms:itemset nodeset="instance('entries')/entry">
<xforms:label ref="title"/>
<xforms:value ref="@id"/>
</xforms:itemset>
</xforms:select1>
<h2>Editor</h2>
<b>Entry ID:</b> <xforms:output bind="sidbind"/><br />
<xforms:input incremental="true" bind="titlebind" >
<xforms:label><b>Title: </b></xforms:label>
</xforms:input><br/>
|
In this case, you've actually added two instances. The first, ui, holds any of the user's choices. You'll start with just the item the user wants to edit, but ultimately you'll use this instance to hold filter information as well.
The entries instance includes several existing entries. In this case, they have a similar structure to the editor, but it's not required, since you'll be setting the values manually.
The page also includes a listing of each of the existing entries, along with a radio button that lets the user choose, as you can see in Figure 4.
Figure 4. Listing the entries
The important thing here is to set up the page so that when the user clicks on one of these radio buttons, the form knows how to move the data from one instance to the other (see Listing 4).
Listing 4. Moving the data
...
<xforms:bind nodeset="instance('editor')/body" id="bodybind"/>
<xforms:bind nodeset="instance('editor')/title" calculate=
"instance('entries')/entry[@id=instance('ui')/currentItem]/title"/>
<xforms:bind nodeset="instance('editor')/body" calculate=
"instance('entries')/entry[@id=instance('ui')/currentItem]/body"/>
<xforms:bind nodeset="instance('editor')/@id"
calculate="instance('ui')/currentItem" />
<!-- Dummy submission; we're just dealing with the client -->
<xforms:submission id="editsubmission" action="savechanges.php"
method="post"/>
|
In fact, when you use XPath, populating the editor form is straightforward. Create three new binding statements, each referring to one of the nodes in the editor instance. You can then populate them using calculate, as before, but in this case, you're choosing an entry in the entries instance based on a value from the ui instance, currentItem. The user sets that value when he or she clicks a radio button, as you can see in Figure 5.
Figure 5. Clicking the radio button
That takes care of the editor. But what if you have more entries than you can easily handle?
Creating a list of unique values
The next step is to create the filter fields so that you can narrow down your choices. You can start with a pulldown menu of authors.
To create a drop-down of authors, you have three choices: manually create the drop-down menu, create an instance of author names, or use the existing author names in the entries instance. Let's choose option number three (see Listing 5).
Listing 5. Filtering by author
...
<h2>Existing entries</h2>
<xforms:select1 incremental="true" appearance="full"
ref="instance('ui')/currentItem">
<xforms:itemset nodeset="instance('entries')
/entry[author/@id=instance('ui')/authorId]">
<xforms:label ref="title"/>
<xforms:value ref="@id"/>
</xforms:itemset>
</xforms:select1>
<h4>Filter results by:</h4>
<xforms:select1 appearance="minimal" incremental="true"
ref="instance('ui')/authorId">
<xforms:label>Author: </xforms:label>
<xforms:itemset nodeset= "instance('entries')/entry">
<xforms:label ref="author" />
<xforms:value ref="author/@id" />
</xforms:itemset>
</xforms:select1>
<br />
<h2>Editor</h2>
...
|
In this case, you've created a drop-down menu, then filtered the entries shown based upon the current value of that drop-down menu by adding a predicate to the itemset nodeset. As you can see in Figure 6, this approach has two problems: first, the list shows duplicate author names. Second, with the filter in place, if the user hasn't chosen an author, no entries come up.
Figure 6. Listing authors
Let's start by fixing the first problem, duplicate author names.
Eliminating duplicates is a problem that often stumps novice XPath users, who frequently come from the SQL world and are stumped by the seeming limitation of predicates versus where and having clauses. Fortunately, it's not a difficult problem (see Listing 6) to fix.
Listing 6. Eliminating duplicates
...
<xforms:select1 appearance="minimal" incremental="true"
ref="instance('ui')/authorId">
<xforms:label>Author: </xforms:label>
<xforms:itemset nodeset=
"instance('entries')/entry[not(author = preceding-sibling::entry/author)]">
<xforms:label ref="author" />
<xforms:value ref="author/@id" />
</xforms:itemset>
</xforms:select1>
...
|
Basically, you are choosing only entry elements that have an author that hasn't already appeared, which is literally what you're trying to accomplish. As you can see in Figure 7, this solves the problem.
Figure 7. No more duplicates
As you can see, when you choose an author, only that author's entries show up. You'll still have to deal with the fact that nothing shows up until then, but let's put that on hold for a moment while you add the other filters.
Creating a list with multiple filters
Adding additional filters requires simply adding additional predicates (see Listing 7).
Listing 7. Adding other filters
...
<h2>Existing entries</h2>
<xforms:select1 incremental="true" appearance="full"
ref="instance('ui')/currentItem">
<xforms:itemset nodeset="instance('entries')/entry[author/@id=instance('ui')
/authorId][category=instance('ui')/category][status=instance('ui')/statusChoice]">
<xforms:label ref="title"/>
<xforms:value ref="@id"/>
</xforms:itemset>
</xforms:select1>
<h4>Filter results by:</h4>
...
<xforms:select1 appearance="minimal" ref="instance('ui')/category">
<xforms:label>Category: </xforms:label>
<xforms:choices>
<xforms:item>
<xforms:label>General</xforms:label>
<xforms:value>General</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Specific</xforms:label>
<xforms:value>Specific</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>NonSensical</xforms:label>
<xforms:value>NonSensical</xforms:value>
</xforms:item>
</xforms:choices>
</xforms:select1>
<br />
<xforms:select1 appearance="minimal"
ref="instance('ui')/statusChoice">
<xforms:label>Status: </xforms:label>
<xforms:choices>
<xforms:item>
<xforms:label>Active</xforms:label>
<xforms:value>1</xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Inactive</xforms:label>
<xforms:value>0</xforms:value>
</xforms:item>
</xforms:choices>
</xforms:select1>
<h2>Editor</h2>
...
|
Here you've added two more pulldown menus, statically created, but the magic is in the XPath statement that defines the itemset. You can add as many predicates to a statement as you want. In this case, the statement selects all of the entries that satisfy the author filter, then out of those, it selects the ones that satisfy the category filter, and then out of those, it selects only those that satsify the status filter, as you can see in Figure 8.
Figure 8. The filters at work
Using this method, you can draw requirements from as many different locations as you like.
Using default values in a filter
There's still one more problem here. There's no way to easily select a condition only if the user has activated the filter, so the user must choose all three filters to get any results. You might think that the solution would be to use a "wild card" as a default value, but XPath doesn't recognize that, either. Instead, you'll have to use a little bit of trickery (see Listing 8).
Listing 8. Using default values
...
<xforms:instance id="ui" xmlns="">
<ui>
<currentItem></currentItem>
<statusChoice> </statusChoice>
<authorId> </authorId>
<category> </category>
</ui>
</xforms:instance>
...
<h2>Existing entries</h2>
<xforms:select1 incremental="true" appearance="full"
ref="instance('ui')/currentItem">
<xforms:itemset nodeset="instance('entries')/entry
[author/@id=instance('ui')/authorId or instance('ui')/authorId=' ']
[category=instance('ui')/category or instance('ui')/category=' ']
[status=instance('ui')/statusChoice or
instance('ui')/statusChoice=' ']">
<xforms:label ref="title"/>
<xforms:value ref="@id"/>
</xforms:itemset>
</xforms:select1>
<h4>Filter results by:</h4>
<xforms:select1 appearance="minimal" incremental="true"
ref="instance('ui')/authorId">
<xforms:label>Author: </xforms:label>
<xforms:choices>
<xforms:item>
<xforms:label>All</xforms:label>
<xforms:value> </xforms:value>
</xforms:item>
</xforms:choices>
<xforms:itemset nodeset=
"instance('entries')/entry[not(author = preceding-sibling::entry/author)]">
<xforms:label ref="author" />
<xforms:value ref="author/@id" />
</xforms:itemset>
</xforms:select1>
<br />
<xforms:select1 appearance="minimal" ref="instance('ui')/category">
<xforms:label>Category: </xforms:label>
<xforms:choices>
<xforms:item>
<xforms:label>All</xforms:label>
<xforms:value> </xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>General</xforms:label>
<xforms:value>General</xforms:value>
</xforms:item>
...
</xforms:choices>
</xforms:select1>
<br />
<xforms:select1 appearance="minimal"
ref="instance('ui')/statusChoice">
<xforms:label>Status: </xforms:label>
<xforms:choices>
<xforms:item>
<xforms:label>All</xforms:label>
<xforms:value> </xforms:value>
</xforms:item>
<xforms:item>
<xforms:label>Active</xforms:label>
...
|
One crucial note: The XPath statements are broken into multiple lines here just to fit them on the page. You MUST put them back together for them to work.
Now, let's start, actually, at the bottom. The status pulldown has a new choice, All, with a value of a single space. The ui instance now has a space as the value for the statusChoice element, so that will be the value shown to start with. The same code has been added for the category pulldown.
The author pulldown is a little bit more complicated. Itemset is only for dynamically created items; choices defines static ones. Fortunately, you can use them both together.
Once you have all of that in place, it's a matter of constructing the XPath statement for showing the entries.
The predicates work like this: for each node, the engine processes the predicate using that context. If the expression is true, the node is added to the results. If it's not, it's not. So what you've done here is created a situation in which if the user has chosen a value for a particular filter, only the appropriate nodes will return "true." On the other hand, if the filter is still set to "All," the second part of the "or" expression will always be true, so all nodes pass the test.
You can see the final page in Figure 9.
Figure 9. The final page
And that's all there is to it.
Summary
XForms is based heavily on what you can do with XPath. Fortunately, that's a lot. In this article, you learned how to use XPath to:
- Automatically populate a node using the results of an XPath function
- Select data in one location based on user choices in another location
- Display only unique items from a list
- Filter results based on multiple criteria
- Provide a wild-card value for an XPath expression
What you've seen here may seem simple, but it's deceptively so. These techniques can be used, without modification, to create the most complicated of XForms user interfaces.
Download | Description | Name | Size | Download method |
|---|
| Article source code | xpathinxforms_source.zip | 2KB | HTTP |
|---|
Resources Learn
-
Get a basic introduction to XForms in Introduction to XForms, Part 1: The new Web standard for forms (Chris Herborth, developerWorks, September 2006), Introduction to XForms, Part 2: Forms, models, controls, and submission actions (Chris Herborth, developerWorks, September 2006), and Introduction to XForms, Part 3: Using actions and events (Chris Herborth, developerWorks, September 2005).
-
Learn more about XForms in the IBM developerWorks XML zone.
-
See where XForms is going.
-
See where XPath is going.
-
Learn the essentials for creating the next generation of forms in XForms basics (Nicholas Chase, developerWorks, October 2006).
-
See extreme examples of using these techniques in a working application in the developerWorks series Use XForms to create an accouting tool.
-
Visit the IBM XForms community.
-
Download the XForms extension for Mozilla.
-
Find out how to accept XForms data in Java (Nicholas Chase, developerWorks, October 2006), Perl (Tyler Anderson, developerWorks, October 2006), and PHP (Nicholas Chase, developerWorks, September 2006).
-
IBM XML certification: Find out how you can become an IBM-Certified Developer in XML and related technologies.
-
XML technical library: See the developerWorks XML Zone for a wide range of technical articles and tips, tutorials, standards, and IBM Redbooks.
-
developerWorks technical events and webcasts: Stay current with technology in these sessions.
-
Learn all about XML at the developerWorks XML zone.
Get products and technologies
Discuss
About the author  | |  | Nicholas Chase has been involved in Web site development for companies such as Lucent Technologies, Sun Microsystems, Oracle, and the Tampa Bay Buccaneers. Nick has been a high school physics teacher, a low-level radioactive waste facility manager, an online science fiction magazine editor, a multimedia engineer, an Oracle instructor, and the Chief Technology Officer of an interactive communications company. He is the author of several books, including XML Primer Plus (Sams). |
Rate this page
|  |