Discussing the nuts and bolts of software development

Monday, April 21, 2008

 

Struts 2 doubleselect: A Detailed Example

Struts 2 provides a fantastic doubleselect tag, which allows you to specify two HTML select lists in such a way that the contents of the second list is dependent on the selection of the first (1). About a month ago, I came across a situation where this type of relationship was very advantageous, and set off towards the relevant Struts 2 documentation to learn how this mysterious tag worked. I found the list of attributes useful, but the examples at the bottom of the page left much to be desired (they're completely hard-coded). In fact, even after a good hour of searching, I couldn't find a solid example of the doubleselect tag that didn't hard-code both lists! Obviously it's much more useful to take advantage of the java back-end, and it took me two full hours of trial and error to finally figure out how to get my particular scenario working. What follows is a detailed example showing how to use the Struts 2 doubleselect tag to its fullest, for the next time I or anyone else wishes to make good use of it.

For our example, suppose you run some sort of automotive website, and would like to let the use specify the Make and Model of a car. The list of Makes should contain the car company, like Porsche, Ferrari or Lamborghini. The lists of Models should be the specific cars those companies make, like "911 Turbo" and "Boxster S" for the Porsche, "F430 Spyder" and "FXX" for Ferrari, etc. So when Porsche is selected in the first list, it's set of models will be shown in the second list. If we change the selection in the first list to Ferrari, the second list should update to contain only the Ferrari models. We also want to know what Make and Model the user has selected, and be able to set default selections in each list. This can actually be done with very little JSP syntax using the doubleselect tag:


<%@ taglib prefix="s" uri="/struts-tags"%>

<s:form action="selectCar" method="POST">
<s:doubleselect
name="makeId" doubleName="modelId"
list="allMakes" doubleList="models"
listKey="id" doubleListKey="id"
listValue="name" doubleListValue="name"
value="defaultItem" doubleValue="doubleDefaultItem" /
>
</s:form>


Of course, in addition to the jsp shown above, we need a struts.xml file, the struts action, a Make class and a Model class. It is assumed that you already know how to set up the struts.xml file and the basics of the struts action. Here's what we need in the other files:
When the page loads, here is how the doubleselect tag is interpreted:
When the form is submitted, the name and doubleName attributes will call setMakeId and setModelId in the action, and give them the values of listKey and doubleListKey respectively. So if Lamborghini is selected in the first list, and Murcielago in the second, the parameters passed in setMakeId and setModelId would be the id of the Make instance which has the name Lamborghini, and the id of the Model instance which has the name Murcielago.

So that's all there is to it! I hope this has been helpful, and saves a bit of time for the next person trying to implement a complex double-list system with back-end support.


(1) This functionality is provided using javascript which is automatically generated when Struts interprets the doubleselect tag. One downside to this approach is that it will clutter up your source quite a bit if you're using a large dataset, but this is unavoidable while using this tag.

Labels: ,


Monday, April 07, 2008

 

Operation Aborted Hell

Do you ever get the dreaded “Operation Aborted” errors on IE6 and IE7? If you have, you have likely spent hours trying to figure out the cause, staring at code, commenting out code and searching the internet looking for solutions.

On my last project I hit these errors too, and here are some of the causes and their solutions:

Cause 1:
You are attempting to modify the DOM before the DOM is ready.

Possible Solution 1:
Move all your code that modifies the DOM to the bottom of the page. Just before the closing BODY tag.

Possible Solution 2:
Defer all your JavaScript execution until after the page has loaded.


<script type="text/JavaScript" defer="defer">
<!—JavaScript code here -->
</script>


A word of warning with this approach, defer is handled differently with different browsers (isn’t this always the case) and the differences are very subtle.

Possible Solution 3:
Use code to determine when the DOM is ready, and then modify the DOM at that point. More on this topic in a follow up post.

Possible Solution 4:
Use the onLoad event to run the code that will modify the DOM. The onLoad event is executed the same across all browsers.

This solution is the cleanest, though not always possible depending on what you want to do.

Cause 2:
You are attempting to modify a parent node of the DOM element you are contained in.

Solution 1:
You cannot modify parent DOM elements from one of its child nodes. Move the JavaScript code up to the same level of the node you are attempting to modify.

Labels: , , ,


Friday, April 04, 2008

 

Creating Dynamic HTML Content

For the uninitiated, the whole concept of generating new HTML through JavaScript can be pretty intimidating, but it certainly remains a powerful and easy way to alter a web page on the fly, especially since all the popular browsers support it nowadays.

The HTML DOM

The Document Object Model is an API which allows you to access and modify the contents of an HTML document through a client-side script such as JavaScript. The current content of the page, as stored in the browser, is represented by a tree of nodes. Any element found within the page (such as a paragraph block

) will correspond to an object (like HTMLParagraphElement), and in turn anything contained within that element (even plain text) will be accessible as childs of that node.

There have been DOMs ever since web browsers started supporting Javascript, but the one used here (and the standard nowadays) is the W3C DOM that is supported (albeit not quite perfectly) by IE 5+ and Mozilla.

One very important thing to understand right off the bat is that the DOM is an interface model. The objects you'll be invoking in your scripts are ways to access the document data, but they're not actual containers. This means you're not going to create them like you create Java objects, and that you'll be able to work with any kind of node on a general level without having to use the specific object for it.

Creating new HTML objects on the fly

The simplest way to generate content is to invoke the createElement() method of your document object.


var myNewParagraph = document.createElement("p");


Simple as that! A new node of type paragraph is created out of thin air, just as if you had entered "<p></p>" in your document source. But where will this new element end up? You'll have to specify that by "inserting" it into your document. For instance:


// Appends the paragraph at the end of the form
document.myForm.appendChild(myNewParagraph);

// Appends it to the beginning of the form
document.myForm.insertBefore(myNewParagraph, document.myForm.firstChild);

// Replaces another element altogether
document.myForm.replaceChild(myNewParagraph, document.getElementById("MyPlaceholder"));


As you can see, there are several ways to get to the proper insertion point. The methods supported by a parent node are appendChild(), removeChild(), replaceChild() and insertBefore(). Any given node also has the following properties to help you traverse the data tree: firstChild, lastChild, nextSibling, previousSibling and parentNode. What more can you ask for?

Altering properties

Adding new elements is all well and good, but what about setting attributes? The easiest way to do so would be with the following "generic" call:


newCell.setAttribute("width", "25px");
newCell.setAttribute("align", "right");
newCell.setAttribute("myAttribute", "myValue");


The setAttribute() method works pretty much as if the text "myAttribute=myValue" had been written inside the element's tag. I say "pretty much" because I've experienced problems with it when I tried formatting everything that way. Style classes, notably, never worked quite right. Because of this, I'd suggest doing all your attribute settings through object properties, like so:


newInput.type = "text";
newInput.style.width = "100%";
newInput.className = "myClass";
newInput.name = "MyUserBox::" + myIndexValue;
newInput.id = newInput.name;
newInput.value = "[Please enter a value]";


Any standard HTML attribute should have a corresponding object property, although the names don't always match - you might need to do a bit of research to find the right property.

Taking shortcuts

By now you're probably annoyed at the idea that you'll have to write several lines of code for each and every HTML element you need to create. Well, you're not the only one. Several shortcuts are supported by the standard browsers. For instance, the following should work just fine:


var myNewParagraph = document.createElement('<p style="text-align: right;">I don\'t see <b>WHY%lt;/b> I should set everything manually' +
'if this approach actually <a href="http://www.macadamian.com/">works</a>!</p>');


Of course, the following approach requires the browser to do some parsing, and is therefore much slower than the official one. But then again, on a modern machine, you're not likely to notice it.

Another gift from Heaven is the "innerHTML" property, which allows you to not only see the HTML representation of what an element contains, but also to edit it on the fly, having it parsed in a similar manner:


var myNewParagraph = document.createElement("p");
myNewParagraph.style.textAlign = "right";
myNewParagraph.innerHTML = 'This can be even <b>COOLER</b> because you don\'t have to create a new object. You could just ' +
'change the property of an old one and it should still <a href="http://www.macadamian.com/">work</a>!';


There is also an outerHTML property which shows the whole tag corresponding to its element, not just what's contained in it. You can even set its content, just like innerHTML, but I wouldn't touch that with a 10-foot mouse.

Tables

Tables elements (TABLE, TR, TH, TD) are trickier than most elements. For one thing, their inner/outerHTML properties are read-only. They should also be added and removed with a different (but overall nicer) syntax:


var myNewRow = myTable.insertRow(); // Add a row at the bottom of the table
var myNewCell = myNewRow.insertCell(); // Add a cell at the end of the row
var myNewFooterRow = myTable.tFoot.insertRow(2); // Add a row before the 3rd one, within the footer
myTable.tBodies[0].rows[0].deleteRow(0); // Delete the 1st cell of the 1st row of the 1st block


insertRow() and insertCell() can take an index stating where the new element goes, or it will simply add it "at the end". Note that you can work with rows at both the table and section levels (deleting the Nth row of the table vs deleting the Nth row of the header), so be careful and know the different between the rowIndex and sectionRowIndex properties.

Final notes and warnings

When dynamically altering your page, be prepared to do a lot of trial and error, and don't forget to test your code on every platform you intend to support.

Finally, if something doesn't work, try a different syntax and/or approach. There's more than one JavaScript way to skin an HTML cat...

Labels: , ,


This page is powered by Blogger. Isn't yours?