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.
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".
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:
- Call
cloneNode() to copy the complete subtree of nodes.
- Iterate through all INPUTs and SELECTs and set the VALUE
attribute to the default value (in Gladiator, we use a dot, "." to
represent the default missing value). We use the
getElementsByTagName() method to get
a list of all the INPUT and SELECT elements contained within the object.
- Iterate through all the NAMEs and IDs and change the suffixes
based on the latest objectCount.
- Iterate through all the event attributes (i.e., the onClicks, onChanges,
and whatever else you might be using) and change the suffixes of any ID
references that we encounter.
- Increment objectCount.
- Set the status attribute on the object to "
new".
- Change the background color of the copied object so the user can
distinguish between existing "
old" records and new records.
- Insert the new object as a child of the container using
appendChild().
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
and
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: