Loop Through Subform Instances

A note today on a specific coding practice I have seen in a number of customer forms. The task is to write a loop that examines each subform instance in a repeating subform definition. The pattern I have seen is where users construct SOM expressions inside a loop and call resolveNode() to find the result. I will show some alternate coding patterns that are easier to write and more efficient to execute. I have included a sample that has five buttons – one illustrating each variation.  The sample looks at each expense row in an expense report and ensure that the description field is populated.

Variation 1: The Customer Solution

Here is the JavaScript as the customer wrote it:

expenseReport.#subform[0].validate1::click – (JavaScript, client)
var nItems = expenseReport.expenses.expense.instanceManager.count;
for(x=0; x<nItems; x++)
{
    var vName = "expenseReport.expenses.expense["+ x +"].description";
    var vDesc = xfa.resolveNode(vName).rawValue;
    if(vDesc == "" || vDesc == null)
    {
        xfa.host.messageBox("Missing description field");
        break;
    }
}

Variation 2: Use the "all" property

In SOM we have the "[*]" notation. "expense[*]" means "all the instances of the expense subform". Unfortunately, we cannot expose the "[*]" notation in JavaScript. Instead, we introduced the "all" property. Using "expense.all", the script can be written without constructing SOM expressions:

expenseReport.#subform[0].validate2::click – (JavaScript, client)
var vItems = expenses.expense.all;
for(i=0; i<vItems.length; i++)
{
    if (vItems.item(i).description.rawValue == null)
    {
        xfa.host.messageBox("Missing description field");
        break;
    }
}

Variation 3: Use resolveNodes()

Both previous solutions assume you have at least one instance of the expense subform to start with. In the case where we might not have any occurrences, we can use resolveNodes().

resolveNode() returns a node object . We use it when we expect our SOM expression to return a single node. Use resolveNodes() (plural) to return a list object when your SOM expression may return multiple objects. In this example, use the expression: "expenses.expense[*].description" to return all the description fields:

expenseReport.#subform[0].validate3::click – (JavaScript, client)
var vDescItems = this.resolveNodes("expenses.expense[*].description");
for(i=0; i<vDescItems.length; i++)
{
    if (vDescItems.item(i).rawValue == null)
    {
        xfa.host.messageBox("Missing description field");
        break;
    }
}

Variation 4: The FormCalc version

If you prefer using FormCalc, it looks like:

expenseReport.#subform[0].validate4::click – (FormCalc, client)
foreach vDesc in (expenses.expense[*].description) do
    if (not hasValue(vDesc)) then
        xfa.host.messageBox("Missing description field");
        break; 
    endif
endfor

Note the use of the hasValue() function. It has the added benefit that it will reject description fields that consist of only whitespace.

Variation 5: The Deep End

The most terse (and most efficient) variation would use a predicate expression:

expenseReport.#subform[0].validate5::click – (JavaScript, client)
if (this.resolveNodes(
          "expenses.expense.[not hasValue(description)]").length > 0)
    xfa.host.messageBox("Missing description field");

(more on predicates in previous blog entries: here and here.)

Other Issues

Validating outside the field

I should have mentioned this disclaimer earlier: I do not like form design patterns where validation logic lives outside the individual fields. I understand the motivation – the lack of control over validation messaging. But there are better design patterns. However, that is a topic for a whole series of blog entries.

Anchor resolveNode

Note the difference between "this.resolveNode()" and "xfa.resolveNode()". The SOM expression passed to resolveNode() is evaluated relative to the object hosting the call. The original customer version used:

xfa.resolveNode("expenseReport.expenses.expense[0].description");

Whereas if the call is anchored under "this" (the validation button context) the SOM expression can be shortened to:

this.resolveNode("expenses.expense[0].description");

Checking empty

The customer version of the script checks:

if (vDesc == "" || vDesc == null)

Unless you have customized your null handling, this check is sufficient:

if (vDesc == null)

By default, all empty text and numeric fields return null.