Building a dynamic table for multivalue handling in an O4W Form
Creating a dynamic table (that can grow or shrink as new values are added or removed, in effect emulating an OpenInsight edit table) using the O4W APIs is relatively straightforward, though there are some additional issues to be aware of.
The table itself is constructed using the standard O4W table APIs - O4WTableStart, O4WTableEnd, O4WSetCell, and optionally O4WTableHeader - along with normal input elements like O4WTextbox. For example, given a dynamic array (named myData) of 3 fields, each field containing 2 values, we can build this into a table with 2 rows and 3 columns (note that typically each _value_ normally becomes its own _row_):
O4WTableStart("ExampleDynamicTable") O4WTableHeader("Date") O4WTableHeader("Time") O4WTableHeader("Location") Num.values = dcount(myData<1>, @VM) For each.value = 1 to num.values this.date = myData<1, each.value> this.time = myData<2, each.value> this.loc = myData<3, each.value> O4WSetCell(each.value, 1) O4WTextbox(this.date, "", "", "DATES", "DATE_":EACH.VALUE) O4WSetCell() O4WTextbox(this.time, "", "", "TIMES", "TIME_":EACH.VALUE) O4WSetCell() O4WTextbox(this.loc, "", "", "LOCS", "LOC_":EACH.VALUE) Next each.value O4WTableEnd("ExampleDynamicTable")
We programmatically determine the number of values, and then build the table (one row per value) in a loop to accommodate all the values.
While this table represents the current data, it does not yet include the ability to insert or delete new data. O4W provides events that can be attached to buttons to allow for insertion, deletion, and addition of rows to a table without requiring any additional O4W basic+ programming (that is, the insertion, deletion, and addition all occur on the browser client). To add this functionality, we must create buttons to trigger these functions, and then use O4WQualifyEvent to attach the events to these buttons. Adding these to the existing example, the code now looks like this:
O4WTableStart("ExampleDynamicTable") O4WTableHeader("Date") O4WTableHeader("Time") O4WTableHeader("Location") Num.values = dcount(myData<1>, @VM) For each.value = 1 to num.values this.date = myData<1, each.value> this.time = myData<2, each.value> this.loc = myData<3, each.value> O4WSetCell(each.value, 1) O4WTextbox(this.date, "", "", "DATES", "DATE_":EACH.VALUE) O4WSetCell() O4WTextbox(this.time, "", "", "TIMES", "TIME_":EACH.VALUE) O4WSetCell() O4WTextbox(this.loc, "", "", "LOCS", "LOC_":EACH.VALUE) O4WSetCell() O4WButton("Ins", "BTN_INS_":EACH.VALUE) O4WButton("Del", "BTN_DEL_":EACH.VALUE) O4WQualifyEvent("BTN_INS_":EACH.VALUE, "INSERTROW") O4WQualifyEvent("BTN_DEL_":EACH.VALUE, "DELETEROW") Next each.value O4WTableEnd("ExampleDynamicTable") O4WButton("Add", "BTN_ADD") O4WQualifyEvent("BTN_ADD", "ADDTOTABLE", "ExampleDyanmicTable", "-1")
In the above code, each row has its own Ins (Insert) and Del (Delete) button, and O4WQualifyEvent is called to bind each button to either the INSERTROW or DELETEROW action. There is also an additional button after the table to add a new row, and it uses the ADDTOTABLE event (for which you must also specify the name of the table and where the new row should be added - use "-1" for bottom and "0" for top of the table).
When one of the rows is inserted or added, the previous row is duplicated entirely and used as the "source" for the new row (though O4W will generate new IDs for each element that has an ID in the duplicated row). This means that any existing text in the O4WTextbox is also duplicated. If this behavior is not desired, you must tell O4W that the information in the textbox should be cleared out when it’s duplicated. This is done by specifying a particular predefined style name on the textbox - "o4wClearDuplicateVal". Modifying our code to include this we end up with the following:
O4WTableStart("ExampleDynamicTable") O4WTableHeader("Date") O4WTableHeader("Time") O4WTableHeader("Location") Num.values = dcount(myData<1>, @VM) For each.value = 1 to num.values this.date = myData<1, each.value> this.time = myData<2, each.value> this.loc = myData<3, each.value> O4WSetCell(each.value, 1) O4WTextbox(this.date, "", "", "DATES", "DATE_":EACH.VALUE, "o4wClearDuplicateVal") O4WSetCell() O4WTextbox(this.time, "", "", "TIMES", "TIME_":EACH.VALUE, "o4wClearDuplicateVal") O4WSetCell() O4WTextbox(this.loc, "", "", "LOCS", "LOC_":EACH.VALUE, "o4wClearDuplicateVal") O4WSetCell() O4WButton("Ins", "BTN_INS_":EACH.VALUE) O4WButton("Del", "BTN_DEL_":EACH.VALUE) O4WQualifyEvent("BTN_INS_":EACH.VALUE, "INSERTROW") O4WQualifyEvent("BTN_DEL_":EACH.VALUE, "DELETEROW") Next each.value O4WTableEnd("ExampleDynamicTable") O4WButton("Add", "BTN_ADD") O4WQualifyEvent("BTN_ADD", "ADDTOTABLE", "ExampleDyanmicTable", "-1")
(There are comparable style names to clear out any plain text in the cell ("o4wClearDuplicateText"), any selected value in a radio button set ("o4wClearDuplicateRadio"), any selected values in a checkbox set ("o4wClearDuplicateCheckbox"), and any selected element in a listbox ("o4wClearDuplicateCombo")).
When this table is submitted to the host (for example, on a CLICK event), all the rows (both those originally created and those dynamically added) will be returned and accessible via the normal O4WGetValue calls. As usual in O4W, all the input elements with the same name will be returned via a single call to O4WGetValue, with each row of data turned into a separate value in the returned response. For example, the following code:
DATES = O4WGetValue("DATES")
Will return all the values from all the DATES textboxes, @VM delimited. Returning data in this format makes it easier to put the data back into a dynamic array that matches the original input format:
DATES = O4WGetValue("DATES") TIMES = O4WGetValue("TIMES") LOCS = O4WGetValue("LOCS") myData<1> = DATES myData<2> = TIMES myData<3> = LOCS
It is critical to remember that while the O4W API call O4WGetValue will get all the values from an input element by using the element’s name, you must use the ID of the element if you wish to update an element’s value using O4WUpdate; updating an element’s value requires that you specifically address the individual element, using the ID which uniquely identifies that particular element (multiple elements can share the same name, but must have different IDs). The API calls that need to address elements by ID (rather than name) will need to be handled differently due to the dynamic nature of the table.
When O4W inserts a row for you, it automatically renames all the elements that have an ID so that they have a unique ID (because the ID _has_ to be unique for correct browser behavior). It builds the unique ID for each element by appending a string to the end of the current ID - a string that starts "_o4wid_". So given the above example, if a row is inserted between 1 and 2, the new ID of the first element is actually DATE_1_o4wid_0; if a new row is added, the new ID is DATE_3_o4wid_1, etc., etc.
Normally, when the information is sent into the host, any element that has "_o4wid_" in it has that part stripped off, and so only the "normal" ID is returned . If, however, you wish to have the full ID (including any "_o4wid_xxx" suffixes) returned, you must tell O4WQualifyEvent that this is a dynamic table and it should leave on the programmatically created extra string. In general, developers can add "_DYNAMIC" on the end of the name of the event to indicate that this is an operation on a dynamic table. So to add a button in the table that sent info to the host on a CLICK event, developers can specify the CLICK_DYNAMIC event instead of just CLICK; to be notified when something’s changed in the table, use the CHANGE_DYNAMIC event instead of just CHANGE; etc. The CLICK, CHANGE, CHANGED, LOSTFOCUS, GOTFOCUS, PRE_FIELD, TAB, and VALIDATE events all support the _DYNAMIC suffix.
For example, to add an additional button that sends an event to the host on each row, the above code could be modified as follows:
O4WTableStart("ExampleDynamicTable") O4WTableHeader("Date") O4WTableHeader("Time") O4WTableHeader("Location") O4WTableHeader("Lookup?") Num.values = dcount(myData<1>, @VM) For each.value = 1 to num.values this.date = myData<1, each.value> this.time = myData<2, each.value> this.loc = myData<3, each.value> O4WSetCell(each.value, 1) O4WTextbox(this.date, "", "", "DATES", "DATE_":EACH.VALUE, "o4wClearDuplicateVal") O4WSetCell() O4WTextbox(this.time, "", "", "TIMES", "TIME_":EACH.VALUE, "o4wClearDuplicateVal") O4WSetCell() O4WTextbox(this.loc, "", "", "LOCS", "LOC_":EACH.VALUE, "o4wClearDuplicateVal") O4WSetCell() O4WButton("Lookup", "BTN_LOOKUP_":EACH.VALUE) O4WQualifyEvent("BTN_LOOKUP_":EACH.VALUE, "CLICK_DYNAMIC") O4WSetCell() O4WButton("Ins", "BTN_INS_":EACH.VALUE) O4WButton("Del", "BTN_DEL_":EACH.VALUE) O4WQualifyEvent("BTN_INS_":EACH.VALUE, "INSERTROW") O4WQualifyEvent("BTN_DEL_":EACH.VALUE, "DELETEROW") Next each.value O4WTableEnd("ExampleDynamicTable") O4WButton("Add", "BTN_ADD") O4WQualifyEvent("BTN_ADD", "ADDTOTABLE", "ExampleDyanmicTable", "-1")
By using the CLICK_DYNAMIC event on each BTN_LOOKUP, the full ID of the button will be passed in on the CLICK event; this ID may include a suffix that begins "_o4wid_" if this is an event on a row that’s been added or inserted. You can then use this returned ID to directly address the specific element via O4WUpdate; in addition, if you need to update the other elements in the same row, you can extract the suffix (everything from "_o4wid_" through the end of the passed-in ID) and append that suffix to the other IDs in the same row to directly update them as well. For example, in the handler for the CLICK event, you may have code similar to the following:
CASE CTLENTID[1,10] _eqc "BTN_LOOKUP" * By checking for just the first 10 characters, this case will catch all the various BTN_LOOKUP controls O4WResponse() * Determine our row number ROWNUM = FIELD(CTLENTID, "_", 3) ;* this will be "1", "2", etc. depending on what our button ID is * determine which suffix applies to us SUFFIX = CTLENTID[COL2(), LEN(CTLENTID)] ;* on rows that were originally created, this will just be "", but on dynamically added rows, this will be "_o4wid_xxx", etc. LOC_ID = "LOC_":ROWNUM:SUFFIX ;* this will give us the ID of the LOC element on the same row - either "LOC_1", "LOC_2", or "LOC_1_o4wid_xxx", etc. O4WUPDATE(LOC_ID, "YOU CLICKED ME", O4WResponseOptions("1"))
In addition to passing in the full ID, events on a dynamic table will also include the additional values "o4wDynamicTable", "o4wDynamicCol", and "o4wDynamicRow" to help identify the current row and column. The "o4wDynamicTable" (if present) indicates that this is a dynamic table that is being operated upon, and o4wDynamicCol and o4wDynamicRow are the 0-based column and row of the element that is generating the event.