by John Brinkman
Have you ever come across “inverted XML”? Take this example:
<column name="first name">Halla</column>
<column name="last name">Ayers</column>
<column name="street address">7466 Etiam Avenue</column>
The question is, how do we bind a “firstName” field to the data value: <column name=”first name”>?
We would much prefer to deal with this data in the form:
<streetAddress>7466 Etiam Avenue</streetAddress>
Then we would bind our firstName field to addresses.row.firstName. Easy.
But we do not always have control over the format of the data that we receive. If we are given the inverted form of the XML we have two choices:
- Convert to non-inverted XML using XSLT
- Bind to the data using predicate expressions
My preference is to avoid XSLT when at all possible. It would be very tricky with this example, because the names have spaces in them, so cannot be re-used as XML element names. Instead we use predicate expressions for binding.
We find predicate filtering in both XPath and E4X.
The XPath expression to identify the “first name” value from the sample is:
The E4X expression is:
The XFA SOM expression is either:
In all these examples, predicates are evaluated the same way. We take each candidate instance of the <column> element and evaluate the predicate expression in the context of that element. When the expression returns true, the element is selected.
The (repeatable) address subform binds to addresses.row[*]
The firstName field binds to $.column.[name == "last name"]
The lastName field binds to $.column.(name.value == "last name")
Data Creation Problem
One significant issue with using predicates for binding is that we cannot easily create new instance data. i.e. it is not possible to infer the XML structure that corresponds to the binding expression. If we were to simply create a new instance of the address subform, the data we create would look like:
To get around that, we create our (empty) data before we create a new instance of the address subform. The script looks like this:
// define the data for a new row
var vNewRowData =
<column name="first name"/>
<column name="last name"/>
<column name="street address"/>
// load the new row of data into the data DOM
addresses.dataNode.loadXML(vNewRowData.toString(), false, false);
// add a new instance of the address subform,
// and tell it to bind to the new data
Notice that the address row has been specified according to the data schema and has been inserted into our data DOM before the address subform was created. Then when we create a new instance of the address subform, it will bind to the data we pre-created.
The Deep End
Using this same data set, it is interesting to apply predicates in a slightly different way. In this example I wanted my form to display the US addresses first and the Canadian Addresses second. The form has separate subform definitions for each. If we had been working with the non-inverted form of the data this would have resulted in a pretty simple binding expression: $.row.[country == "US"]
But because of the inverted form of the data, the binding expression gets very complicated. We want to qualify the row element based on the column where name="country" and the value is "US". To get there we use a nested predicate to bind the row:
$.row.[ $.resolveNode("column.[name==""country""]") == "US" ]
The outer predicate filters <row> and the inner predicate filters <column>.