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.

2 Responses to Loop Through Subform Instances

  1. Kevin Penner says:

    I have a simple PDF created in Livecycle Designer containing “Loop Through Subform Instances” code.The problem I am having is when I save the PDF, and then reopen it, the data in the input fields are no longer where I originally typed them in.The PDF is saved as a version 8 Dynamic PDF and the Form properties is set to ‘Automatic’ (that is, not manual).My PDF can be downloaded at:https://share.acrobat.com/adc/document.do?docid=bcb8f652-f468-4c81-b221-65a5e397c89d

  2. I can’t say for sure, because I don’t know your application well enough — but I suspect you can’t model this with a table.I think you need a hierarchy of rows — which are not supported by tables. (“table sections” are subformSets — which don’t introduce hierarchy.)Try modelling this as simple nested subforms. Create your “details” and “rooms” children under a grouping “sect” subform. It should work the way you expect.