Skip to main content

skip to main content

developerWorks  >  XML  >

Cool things you can do with XPath in XForms

developerWorks
Document options

Document options requiring JavaScript are not displayed

Discuss

Sample code


Rate this page

Help us improve this content


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
final form

You'll start by creating the basic form.



Back to top


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
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.



Back to top


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
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.



Back to top


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
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
Clicking the radio button

That takes care of the editor. But what if you have more entries than you can easily handle?



Back to top


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
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
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.



Back to top


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
The filters at work

Using this method, you can draw requirements from as many different locations as you like.



Back to top


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
the final page

And that's all there is to it.



Back to top


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.




Back to top


Download

DescriptionNameSizeDownload method
Article source codexpathinxforms_source.zip2KBHTTP
Information about download methods


Resources

Learn

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


Please take a moment to complete this form to help us better serve you.



 


 


Not
useful
Extremely
useful
 


Share this....

digg Digg this story del.icio.us del.icio.us Slashdot Slashdot it!



Back to top