Skip to main content

Use JavaScript to make your XForms more robust

Michael Galpin (mike.sr@gmail.com), Software Engineer, eBay
Michael Galpin has been developing Web applications professionally since 1998. He is a software engineer at eBay in San Jose, CA. He holds a degree in mathematics from the California Institute of Technology.

Summary:  Have you ever had an XForm where you clicked the Remove button until all the rows disappeared, and then tried to insert a row back? What happens? Nothing! That's what this article will show you how to solve using JavaScript. See how to replace the standard delete action with a smarter JavaScript function that is called by your trigger. Along the way you'll see how to cleverly use JavaScript to manipulate your model's data.

Date:  24 Jul 2007
Level:  Intermediate
Activity:  2105 views

Prerequisites

This article is based solely on XForms and JavaScript. It was tested against the Mozilla XForms plugin installed on Mozilla Firefox 2.0. Everything used is standard XForms and JavaScript, so it should run on other implementations of these two standard technologies. No server-side technologies are used.

The classic table example

Let's take a look at a classic example of XForms. It shows how to create a table representing repeated nodes in an XML document. It shows, among other things, how to use XForms to perform aggregate calculations and how to use XForms to add or remove nodes from the model's data with the view being automatically kept in sync. Take a look at the full source code in Listing 1.


Listing 1. Classic XForms table example
                
<?xml version="1.0" encoding="UTF-8"?>
<xhtml:html xmlns:ev="http://www.w3.org/2001/xml-events" 
xmlns:xforms="http://www.w3.org/2002/xforms" 
xmlns:xhtml="http://www.w3.org/1999/xhtml" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xhtml:head>
        <xhtml:title>Demonstration of table with 
                                      column total</xhtml:title>
        <xf:model id="my-model" xmlns="http://www.w3.org/1999/xhtml"
 xmlns:xf="http://www.w3.org/2002/xforms">
            <xf:instance id="my-data" src="my-data.xml" xmlns=""/>
            <xf:bind calculate="sum(../Item/Amount)" nodeset="/Data/Total"/>
            <xf:submission action="my-data.xml" id="update-from-local-file"
 instance="my-data" method="get" replace="instance"/>
            <xf:submission action="my-data.xml" id="view-xml-instance"
 method="get"/>
            <xf:submission action="my-data.xml" id="save-to-local-file"
 method="put"/>
        </xf:model>
    </xhtml:head>
    <xhtml:body>
        <xf:group ref="/Data" xmlns="http://www.w3.org/1999/xhtml" 
xmlns:xf="http://www.w3.org/2002/xforms">
            <xf:label/>
            <xf:repeat id="repeatItem" nodeset="Item" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:input class="item-description" id="description-input" 
ref="Description" xmlns="http://www.w3.org/1999/xhtml">
                    <xf:label/>
                </xf:input>
                <xf:input class="item-amount" ref="Amount" 
xmlns="http://www.w3.org/1999/xhtml">
                    <xf:label/>
                </xf:input>
            </xf:repeat>
            <xhtml:div id="sum">
            <xf:output ref="/Data/Total" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label/>
            </xf:output>
            </xhtml:div>
            <xf:trigger id="insertbutton" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Add Item</xf:label>
                <xf:action ev:event="DOMActivate">
                    <xf:insert at="last()" nodeset="Item[last()]" 
position="after"/>
                    <xf:setvalue ref="Item[last()]/Description" value="''"/>
                    <xf:setvalue ref="Item[last()]/Amount" value="0"/>
                    <xf:setfocus control="description-input"/>
                </xf:action>
            </xf:trigger>
            <xf:trigger id="delete" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Delete Item</xf:label>
                <xf:delete at="index('repeatItem')" ev:event="DOMActivate" 
nodeset="Item[index('repeatItem')]"/>
            </xf:trigger>
            <xf:submit submission="update-from-local-file" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Reload</xf:label>
            </xf:submit>
            <xf:submit submission="save-to-local-file" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Save</xf:label>
            </xf:submit>
            <xf:submit submission="view-xml-instance" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>View XML Instance</xf:label>
            </xf:submit>
        </xf:group>
    </xhtml:body>
</xhtml:html>

You may notice from the source code that the data in our model is coming from an external XML file. That file is shown in Listing 2.


Listing 2. XML data for example
                
<?xml version="1.0" encoding="UTF-8"?>
<Data>
   <Item>
      <Description>Furniture</Description>
      <Amount>1000</Amount>
   </Item>
   <Item>
      <Description>Dock</Description>
      <Amount>2000</Amount>
   </Item>
   <Item>
      <Description>Boat</Description>
      <Amount>3000</Amount>
   </Item>
   <Item>
      <Description>Lawn equipment</Description>
      <Amount>4000</Amount>
   </Item>
   <Item>
      <Description>Hot tub</Description>
      <Amount>5000</Amount>
   </Item>
   <Total>15000</Total>
</Data>


Running the example

You can run the example by simply opening it in a Web browser. You should see something that looks like Figure 1.


Figure 1. Classic XForms example
Classic XForms example

Try it out and notice you can add or remove rows using the Add Item and Delete Item buttons shown. For example, if you click the Delete Item button once, you should see something similar to Figure 2.


Figure 2. One item removed
one item removed

Notice how not only did the top item get deleted, but the total was recalculated. This is a great example of the power of XForms. Click the Delete Item button four more times and you should get something that looks like Figure 3.


Figure 3. All items removed
All items removed

That's kind of unsightly, but you can just click the Add Item button and start re-entering data, right? It turns out that clicking the Add Item button will do nothing in this situation.


What's broken?

So why does the Add Item not work in this situation? If you look at the source code, the Add Item button causes an insert to our model, adding a record using the XForms insert command. That record happens to be an "Item" node, so the Add Item action then sets some default values for the Item's Description and Amount nodes. The Add Item defines what kind of node to insert by essentially cloning the last node in the structure. That's what the nodeset="Item[last()]" does. That's the source of your "bug." If you eliminate all the Items, then there is nothing to clone and thus nothing to insert. So the Add Item button fails. Of course the question now is, how do you fix this?


The fix

Like most problems in software engineering, there are many ways to solve the problem described here. The solution demonstrated here uses JavaScript. Your strategy will be to change the way you delete. You can see in Listing 1 that the Delete Item button uses the XForms delete command. You'll replace the call to this command with a call to a JavaScript function. That function will have to interact with the XForm model. You will only modify the call to the Delete Item button, so you don't have to change the Add Item button. Let's take a look at the JavaScript solution.


The JavaScript

As mentioned, the idea will be to only change the Delete Item button, not the Add Item button. So for the Add Item button to work, you can never delete all of your data from the model. So you'll keep track of how many items are around, and when you get down to the last one, you won't delete it. You'll just replace its contents with the default contents, like if you did the Add Item button. Let's take a look at the JavaScript code in Listing 3.


Listing 3. JavaScript deleteItem() function
                
<xhtml:script type="text/javascript">
                 //<![CDATA[
                      function deleteItem(){
                           var model = document.getElementById("my-model");
                           var instance = model.getInstanceDocument("my-data");
                           var dataElement = instance.getElementsByTagName("Data")[0];
                           var itemElements = dataElement.getElementsByTagName("Item");
                           var cnt = itemElements.length;
                           if (cnt > 1){
                                dataElement.removeChild(itemElements[cnt-1]);
                           } else {
                                // last element so just set its data to default vals
                                var descripElement = 
itemElements[0].getElementsByTagName("Description")[0];
                                descripElement.childNodes[0].nodeValue = "";
                                var amtElement = 
itemElements[0].getElementsByTagName("Amount")[0];
                                amtElement.childNodes[0].nodeValue = "0";
                           }
                           model.rebuild();
                           model.recalculate();
                           model.refresh();
                      }
                 //]]>
            </xhtml:script>

Here's how this works. You need to access the XForms model from your JavaScript. Luckily this can be done easily using JavaScript's DOM APIs. The XForms model is part of the page's DOM, so you just use the document.getElementById() method, just like you would do to access an HTML div or an HTML input field.

When you access your XForms model using document.getElementById(), as shown in Listing 3, what you get is a nsIXFormsModelElement. This object has several very useful methods on it, including the getInstanceDocument() method used above. This gives you access to your XForms instance defined in Listing 1. This is a DOM object representing the XML document seen in Listing 2. So in your JavaScript code, you simply walk the DOM to get the Item elements. You determine how many Items there are in your model and store it in the cnt variable. There are obviously two use cases to deal with here. The first is if you have more than one Item, and the second is if you have only one. In the first use case, you need to delete the item.


Deleting the item with JavaScript

So how do you delete one of your Item records using JavaScript instead of the XForms delete control? The solution is surprisingly easy. The getInstanceDocument() method called earlier gave you a true DOM object. So you can do anything on this object that you would do with any other DOM object. It supports the full DOM API. So you simply use the removeChild() method on your DOM element. You remove the Item at index (cnt - 1), since your array of elements (obtained by calling getElementsByTagName("Item")) is 0-indexed. No magical XForms APIs here, just straightforward DOM programming.


Deleting the last item

You've reproduced the logic that you would normally accomplish using the XForms delete command. You did this so you could more gracefully deal with the case where you were deleting the last Item. Now take a look at that corner case, such as, cnt == 1.

As mentioned earlier, the key here is that you do not want to actually delete the last Item. If you do that, then the XForms insert command used in the Add Item action will no longer work. Of course, you can also re-code that as well, but it's best to try to avoid that.

So instead of deleting the last Item, you'll just replace its contents with a blank Item. This is the same kind of Item you get when you click the Add Item button. For instance, it has an empty string for its Description and 0 for its Amount. To do this, you once again just use the DOM API. You simply access the last Item element, and then access that element's Description and Amount elements. We set the values of their text nodes to the empty string and 0, respectively.


Synchronizing the view and the model

At this point you've handled both of your use cases for Delete Item. There's still a little more work you need to do. Normally when you use XForms commands to modify the XForms model, all recalculations and view refreshing is done automatically. This is not the case when you use JavaScript to access these same things. You need to do these same things manually.

Luckily, the nsIXFormsModelElement object you got a reference to at the beginning of the deleteItem() function has some more useful methods that will help out. The first thing you use is its rebuild() method. This method causes it to rebuild its internal representing of the data in the model. It essentially syncs up the model object with the DOM. This doesn't affect the view at all, only the model.

Next you need to use the model's recalculate() method. You need to do this because you were keeping track of the sum of the amounts in your table using an XForms bind-calculate command in your model. That calculation gets refreshed when you call the recalculate() method. Again, this is only syncing up your model with the DOM. It does not affect the view at all.

Now that the model is in sync with the data, you can re-paint your view. You do this by calling the refresh() method on the model. This method causes all controls bound to your model to be refreshed. For the case where you are deleting a row, this will cause that row to disappear. In the case where you are deleting the last row, it will cause the data in that last row to be changed to the blank data specified. In both cases, this will cause the total being displayed to be updated based on the current data.


Triggering the JavaScript

Now that you've written a clever JavaScript function to handle your deletes, you just need to modify your XForm so that it calls this JavaScript when the Delete Item button is invoked. To do that, you simply modify the XForms declaration for Delete Item, as shown in Listing 4.


Listing 4. New delete item XForm control
                
<xf:trigger id="delete" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Delete Item</xf:label>
                <xf:load ev:event="DOMActivate"
 resource="javascript:deleteItem()"/>
            </xf:trigger>

Notice that you simply replaced the XForms delete command with a load command that references the deleteItem() JavaScript function. That's the last change you need to make, so let's run the modified example.


Running the modified example

Simply load the example into your browser. It should look just like the original shown in Figure 1. However, when you get down to the last row and click Delete Item, you should see the result shown in Figure 4.


Figure 4. Deleting the last row
deleting the last row

Now the last row won't disappear. Instead it will simply go to default values. If you click the Add Item button, you'll see something similar to Figure 5.


Figure 5. Add Item after deleting Last Item
Add item after deleting last item

The Add Item button still works, with no modification.

Summary

You've seen how you can create a smarter delete item action using JavaScript. More importantly, you've seen how JavaScript can be used to access and modify XForms model data, recalculate XForms-bound calculations, and refresh XForms views. You can imagine several other ways you could implement the Add and Delete Item actions to provide more and more sophisticated functionality. Hopefully, you can also see how to use some of these techniques to improve your own XForms-based applications.



Download

DescriptionNameSizeDownload method
Article sample codexformsjavascript_source.zip2KB HTTP

Information about download methods


Resources

Learn

Get products and technologies

Discuss

About the author

Michael Galpin has been developing Web applications professionally since 1998. He is a software engineer at eBay in San Jose, CA. He holds a degree in mathematics from the California Institute of Technology.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML
ArticleID=242063
ArticleTitle=Use JavaScript to make your XForms more robust
publish-date=07242007
author1-email=mike.sr@gmail.com
author1-email-cc=ruterbo@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers