Gladiator Document Object Model
Node Replication Demonstration

Introduction

One-to-many relationships are often present in database applications. For example, you might need to record several visits to the doctor by one patient, several items purchased by one customer, or several books written by one author.

In the Gladiator project, we wanted to be able to enter a number of eye conditions on one patient all at once and only have to press the Save button once. To do this, we realized that we would need a web form in which we could, after completing one row, add a second, third, or fourth row as we continued to enter additional data.

The basic effect can be achieved using the DOM cloneNode() method. The cloneNode() method will clone a node and all of its child nodes. This provides the base functionality, but there are numerous details that one needs to deal with. This page explains how we implemented the functionality that we wanted, and provides a demonstration of that functionality.

NOTE BENE: As of this writing (2004.02.17), Mozilla is the only browser in which this page works correctly! Only some of the functionality works in Opera and Konqueror, and nothing seems to work correctly in Internet Explorer.

Implementation

Conceptually, we have a container that contains one or more objects. An object is itself a container for the widgets.

Object layout diagram

In practice, the container may be the TBODY of an HTML TABLE and the objects are most naturally then TR elements. Alternatively, the container could be a DIV of a "containerClass", and the objects DIVs of an "objectClass".

The former case (using TBODY and TRs) represents a fairly standard grid layout. The latter case (using DIVs) may represent a standard grid layout, or something more original, such as a tab card layout. This is why I prefer to use the generic noun "object" here instead of something more specific like "row".

Traditional vs. alternate layouts

In any event, the objects contain the form widgets. In Gladiator, a widget comprises the form INPUT element along with associated HTML elements, CSS for styling (absent from my diagram to the left), and Javascript code to give the widget certain behaviour as well as provide validation of data entered by the user.

In Gladiator we use PHP to generate the widget's HTML code. Widgets that we have include text boxes and check boxes with associated labels, spin widgets with labels, and drop-down selection boxes with labels. You can see all of these below.

Using the Document Object Model extensively allows us to write generic code that works regardless of whether the container element is a TBODY, a DIV, or something else. We simply assign an ID to the container element and use the Javascript getElementById() method to obtain a reference to the container when needed.

In the container element, we also add an objectCount attribute so that we know how many objects (or rows, if you like) we have started with. This is of course necessary because we might need to display a number of rows representing existing records in our database. As new objects are added by the user, we simply increment the objectCount. We use the getAttribute() and setAttribute() methods to get and set the objectCount respectively.

When the subtree of nodes for the object is replicated using cloneNode(), of course any values that happen to be entered in the INPUT or SELECT elements are also replicated unchanged. We have to fix this. And naturally all of the NAMEs and IDs that we have assigned to our INPUTs and SELECTs are also copied unchanged. Non-unique IDs are of course useless. Finally, just to make life more interesting, all references to IDs passed as parameters to onClick or onChange event handlers are also unchanged.

So, we have our work cut out for us. We proceed as follows. First, for any one INPUT or SELECT element, we make sure that the NAME and ID are the same. The NAMEs get used when the name-value pairs are sent back to the server for processing by PHP. The IDs get used for all of the Javascript manipulations.

To make all of the NAMEs and IDs unique, we add a square-bracketed ordinal number as a suffix, and then change the suffix for each copied object. Our original template object uses "[0]" as the ordinal. Each subsequent object uses an ordinal suffix based on the current objectCount attribute for the container.

So, to create a new object, we proceed as follows:

Deletion is handled as follows. If an existing "old" record is deleted by the user, its status attribute is changed to "del". This information will be sent back to the server for processing by PHP. On the other hand, if the user wants to delete a "new" record, we just call deleteNode() and are done with it!

When the user presses the Save button, we iterate through all of the object nodes and delete any unchanged "old" records. Since there are no changes, there is no reason to send this data back to the server.

But wait a minute! How do we know if data on existing records have been changed or not? We do it this way: When the form is initially presented to the user, we call an onLoad event handler in the BODY tag to fill in values for existing records from a Javascript array. The inlined Javascript array is of course generated by our PHP script. Since the array of original values is present on our page, we simply compare the values in the array against the values in the form for all of the "old" records. If we find differences, we mark the status attribute on the object as "chg" (changed).

By storing the original values in a Javascript array, we completely avoid the problem of having to assign onChange event handlers on each and every INPUT and SELECT. Of course we can still assign onChange or other event handlers to validate entries if we need to, but our node replication code remains completely independent of any form validation code. In this way, we can continue to use our widget data validation code unchanged and still create dynamic forms using DOM node replication.

Try It!

Below we have constructed two containers.

The first container (light blue grid below) uses a standard HTML TABLE for layout. The TBODY represents the container and the TR rows represent the objects. For testing, we have placed a random assortment of widgets inside the object row: checkbox, text entry, date entry widget, spin widgets, and drop-down selection widgets.

The second container (with the gold border below) uses DIVs for both the container and objects. Again, a random selection of widgets have been used for testing.

Try pressing the Create a new row and Delete a row buttons to see what happens. After adding a deleting a few rows and entering data in the widgets, press the Submit button at the very bottom of the page:

Enroll date:
Age:
Suffix:
State:
Insert a new row after this one Delete this row
Enroll date:
Age:
Suffix:
State:
Insert a new row after this one Delete this row

Unlike above, this container is just a div. The object rows are also just divs. No tables are used for laying out the widgets (note however that the Gladiator widgets do use tables themselves). The widgets below are the same as those above, but appear somewhat different because of additional CSS styling rules.

Insert a new row after this one Delete this row
Smoke start age:
Smoke end age:
Cataracts:
Alcohol amount:

Download

You can download this example page and accompanying Javascript and CSS here:

GladiatorDOMNodeReplicationDemonstration-1.0.0.tar.gz