Posts in Category "validations"

Exclusion Groups v4.0

December 5: Happy Sinterklaas! I hope your children did not find a lump of coal in their boots this morning.

I received some good feedback from the “build your own” exclusion groups introduced in a series of previous posts (part 1, part 2, part 3). These exclusion groups have enriched functionality not found in standard radio button groups.

Since then it has been pointed out that the strategy of using a subform to contain the group is not always practical. In particular there are two cases where using a subform does not work:

  1. PDFs with fixed pages (artwork PDFs). In this case, Designer does not allow you to add subforms to the form.
  2. The case where the exclusion group is spread across cells in a table. In this case, it is not possible to wrap the cells in a container subform. We cannot use the row subform, since there are other cells in the row that are not part of the exclusion group.

The new sample provided here allows you to create an exclusion group by providing an array of fields (or subforms) to include in the group. To make this possible, I have added two new methods to the scGroup script object:

/**
* addArrayGroup – Create an exclusion group by providing an array of
* containers (fields/subforms).
* Call this method from the initialization script of a field/subform.
* Note that a single initialization script can create multiple
* groups.  Once the groups have been initialized, you need to call
*
checkGroups() from the calculate event.
* @param vHost – The field or subform hosting the array group.
* @param vName – a name for the group (unique within this host)
* @param vList – An array of fields or subforms that will make up
*                the group
* @param vMin  – The minimum number of elements in the group that
*                need to be selected
* @param vMax  – The maximum number of elements in the group that
*                may be selected
* @param vValidationMessage – The message to use if the minimum
*    number of objects have not been selected
*    (and if the object does not already have a validation message))
*/
function addArrayGroup(vHost, vList, vMin, vMax, vValidationMessage)

/*
* makeArrayExclusive – Enforce the min/max constraints of an
* array-based exclusion group.
* @param vHost — the container object that hosts the exclusion group
* definition.  
* Note that this will check all groups that this container is hosting
* In order to use this, you will need to call addArrayGroup() from
* the initialization event. This call needs to be placed in the
* calculation event of the same object.
* @return true
*/
function makeArrayExclusive(vHost)

The first example places these calls in the initialize and calculate events of a subform.

The second example adds a hidden field to the table to act as the host. The initialize event of the field calls scGroup.addArrayGroup() to define three groups:

form1.NPSA3.NPTable.Row1.groupControl::initialize – (JavaScript, client)
scGroup.addArrayGroup(this,
                      new Array(secondLine_yes, secondLine_no),
                      0, 1);

scGroup.addArrayGroup(this,
                      new Array(newCustomer_yes, newCustomer_no),
                      0, 1);

scGroup.addArrayGroup(this,
                      new Array(Bundle_yes, Bundle_no),
                      1, 1, "Must select yes or no.");

The calculate event calls scGroup.makeArrayExclusive() to enforce the exclusive behavior:

form1.NPSA3.NPTable.Row1.groupControl::calculate (JavaScript, client)
// Make sure the exclusion group behaviour is enforced
scGroup.makeArrayExclusive(this);

Note that if you create a lot of these on your form, it becomes expensive to clear the error list and recalculate everything. Fortunately you should not have to do that too often – just when removing subforms or when inserting subforms in the middle of a list. In these cases we need to clear/recalculate in order to update all the SOM expressions we have stored.  Have a look at the script in the subform remove buttons for an example.

The Deep End

The set of script objects that support validation and exclusion groups continues to grow in size and complexity.  By this point I expect most form developers would not be comfortable modifying them, but would want to treat them as a ‘black box’ library.  This is fine.  As far as I know, they continue to work back to Reader 7.0. 

To implement exclusion groups as arrays, I created a JavaScript base class with two derived implementations — one for cases where the group is hosted by a subform and one where the group is controlled by an array.  Along the way I also fixed a couple bugs that were uncovered once I started placing the groups inside repeating subforms.

This will not be the last time I enhance these libraries.  I have some more ideas for them.  But that would be a different blog post.

Validation Patterns: Part 3

Here is the last (for now) in the series on how to validate templates in a user-friendly manner.  I have attached a new and improved sample (with data).

What is new in this version:

  • An email submit button that becomes active only when there are no errors on the form
  • A listbox field that shows all the errors on the field.  When you enter into the list field, the field corresponding to the error gets highlighted.  Try selecting different elements in the list.

But more importantly, this sample form now holds a toolkit of functions that allow you to take control of the way Reader validates form fields.

Behind the scenes on the script side, there is a fair bit more in the toolbox.  My goal is that you can re-used these script objects in your forms and keep your form designs as clean and simple as possible.  If you look at the form you will see that the vast majority of script is inside the script objects.  The fields and subforms on the form itself have minimal amounts of script.

An added benefit of following this particular form development methodology is that is should be very consistent with future enhancements we add to XFA and Reader.

Here is a summary of the script functions that you can use in your form development:

/**
* setStatus(vContainer, vStatus)
* Call this method as the last line of your validation script.
* This function does two things:
* 1) highlights or resets the field according to whether it is valid or not
* 2) If invalid, logs the error
*
* @param vContainer — the container we are validating. 
*                      If a subform, process recursively.
* @param validStatus — true | false
* @return the status for the validation script to use 
*         (for now hardcoded to "true")
*/

/**
* formHasErrors()
*   Useful for changing the state of form objects depending on if there are
*   validation errors or not.
*   Place this call inside a validation or calculation script and make sure
*   you have called registerListener(this) from your initialization event.
* @return true if there are errors.  False otherwise.
*/

/**
* registerListener(vListener)
*   Call this from you initialization event if you want to be able to
*   call formHasErrors() in your calculation or validation events.
* @param vListener — The object that cares about whether there are errors. 
*   We’ll keep track of all the listeners. Every time a field changes valid
*   state (becomes valid or invalid), we will loop through all listeners and
*   force their calculate and validate scripts to run.
*   If a listener is a choiceList object, we’ll synchronize the choicelist
*   entries with field errors.
*   When a choicelist has zero entries, we hide it.
* @return void
*/

/*
* setFieldMandatory(vObject, vMandatoryState, vForce)
*   Mark a field or exclusion group as being mandatory.
*   When calling this from a validation script,
*   be sure to also call setStatus()
*
* @param vObject — the field or exclusion group that we’re marking
* @param vMandatoryState — true or false 
* @return boolean — true if the state changed
*/

/**
* clearErrList()
* Call this method before removing a subform or re-ordering subforms.
* Our tracking is based on storing SOM expressions.
*
The SOM expression for a field can change if the order of its parent
* subform changes. 
* After clearing the list and removing/moving subforms,
* call xfa.form.execValidate() to rebuild the list.
* Note that it is not necessary to call this method when appending new subforms.
*/

Then there are a couple of methods you might choose to modify for your own use — in case you do not like the way the sample highlights invalid fields:

/** 
* highlight(vObject)
*   Highlight a field (or exclusion group or subform) to indicate that it
*   has a validation error
* @param vObject — the subform/field/exclusion group to highlight
*/

/**
* unhighlight(object)
*   reset the form field/subform/exclusion group to the state it was in
*   the template.
* @param object — the subform/field/exclusion group to highlight
*/

 

The one thing that does not work back to Reader 7 is the button script that sets focus on an error field.  xfa.host.setFocus() came later.

But the rest of script functionality should work fine in Reader 7. The hardest part about making the scripts work in Reader 7 was that the convenience methods for manipulating choice lists are not available there.  The script has to manipulate the XML structures directly. 

Validation Patterns: Part 2

Continuing from the previous post, we are looking at the problem of validating fields without inundating the user with message boxes during their form session in Adobe Reader — and without centralizing all the validation logic.

In the previous post, we established a design pattern for highlighting fields where a script validation fails. In this post we will deal with mandatory (required) fields.  Today, when you mark fields as mandatory, then any time your form is validated you will get an error message for each missing field.  We can do better.

The updated sample (with data) builds on the previous sample. The strategy is to

  • author the form fields using the standard XFA “required field” settings
  • At runtime disable the “required field” setting and store our own mandatory flag elsewhere in the field
  • Use our setStatus() function during validation to monitor whether fields are populated or not.

Changes from the previous sample:

  • Marked various fields as being mandatory
  • Additional logic in scValidate.setStatus() to cause empty mandatory fields to show up as invalid
  • A new script function: scValidate.setFieldMandatory()

Storing the mandatory state in a field involves a technique described in a previous post where we store a variable under a field.desc element.

Once we have logic in the form that stores our mandatory state, we let the XFA processor believe that no fields on the form are required. This way we don’t get any warnings from Acrobat/Reader, but we graphically modify the fields to highlight them for the user.

The only deviation from standard form design is that for mandatory fields you need to add a call to utility.setStatus() in the validation script – even if there is no script validation required.  i.e. if you mark a field as mandatory, you need to add this validation script:

expenseReport.details.empID::validate – (JavaScript, client)
scValidate.setStatus(this, true);

Conditionally Mandatory

On the sample form, when the payment type field choice is "Direct Deposit", there are three more fields that need to be filled in. If the payment type is any other choice, these fields do not need to be filled in.

Normally we toggle a field’s mandatory status by setting the property: field.mandatory. But now that our design pattern for mandatory fields has co-opted the XFA mandatory mechanism, we will use one of our new global methods: scValidate.setFieldMandatory(vField, vMandatoryState).

For this to work, the form defines validation scripts on the financialInstitution, branchNumber and accountNumber fields that look like this:

expenseReport.payment.deposit.financialInstitution::validate

var vMandatory = directDeposit.rawValue == 1;
scValidate.setFieldMandatory(this, vMandatory);
scValidate.setStatus(this, true);

Updated Exclusion Groups

If you are looking at the script code you will notice that I have included the exclusion group script objects that were defined in a previous post.  The form has placed the direct deposit subform in an exclusion group with the other payment types. I have modified those scripts so that they work with the validation framework. (You will notice I have also broken them out into separate script objects with a new naming scheme).

The exclusion group sample has been updated so that:

  • When an exclusion subform has not yet reached its minimum number of entries, the subform gets highlighted instead of the individual fields in the subform.
  • The call to scGroup.setMinAndMax() now includes an optional validation message parameter.  This is needed because designer does not yet expose a UI for assigning a validation message to a subform.

Note that this sample now introduces the concept of an invalid subform.  If the user has not filled in all the credit card fields, then we mark the subform as invalid — similar to the case where the subform exclusion group did not reach its minimum number of entries.  The script to do this looks like:

form1.#subform[0].Payment.CC.CardType::validate – (JavaScript, client)
// Valid state is where either all fields are null or all fields are populated.
var vValid = (CardType.isNull  && CardNo.isNull  && Expiry.isNull) ||
             (!CardType.isNull && !CardNo.isNull && !Expiry.isNull);

// populate the subform validation message so that users get a meaningful message
if (this.parent.validationMessage.length == 0)
    this.parent.validationMessage = "All credit card fields must be filled in";

scValidate.setStatus(this.parent, vValid );

The validation button

The samples have also updated the logic for the button to validate the form.  Now when there are no errors, the button caption will be updated to say: "no errors" (and will be made read-only).  This works as long as you do not change the name of the button from : "checkValid".  You might prefer a variation on this where the button is hidden when there are no errors.

Backward compatibility

We would like to make it easier you to control validation message handling in a future version of Reader, but for the here and now, you are likely targeting Reader 7/8/9 and need an immediate solution.  You can take these script objects, copy them into new forms and use them in forms compatible as far back as Reader 7.

Next up: Another error reporting option

Validation Patterns: Part 1

Time to tackle my pet peeve. The aspect of customer written script that concerns me most is how customers validate field data. The number one reason for script bloat is that our users bypass the standard XFA/Reader validation framework.

Of course, there is a reason why script writers bypass validation. The problem is that each validation message appears in an individual dialog box. In some cases when a form is validated, the user may have to dismiss dozens of dialogs. Most users would prefer no dialog boxes at all – just show the invalid fields graphically. This is a problem we would like to solve in a product release, but in the mean time, you need forms that work in previous versions of Reader. Starting today I will tackle this topic in a series of blog entries.

Current Practise

Form authors currently work around the “validation message” issue by moving their validation logic outside the prescribed validation mechanisms. They typically place all the validation logic for their form under the click event of a “validate” button. They end up with a very large chunk of script that look like:

if (purchaseOrder.amount.rawValue >= 1000) {
     errorMessage += "Amount must be less than 1000\n";
     errorFlag = true;
}
if (purchaseOrder.quantity.rawValue == null || quantity.rawValue == 0) {
     errorMessage += "Quantity must be specified\n";
     errorFlag = true;
}
... repeat for each business rule ...
if (errorFlag) {
    xfa.host.messageBox(errorMessage);
}

The result is that field definitions are no longer encapsulated (self-contained). The problems introduced:

  • Fields cannot be copied/pasted/moved/deleted/renamed without breaking script
  • Adding new a field requires updating a global script – hard to correlate which fields have validations script and which do not.
  • Validation messages are stored and handled in script. Any attempt to isolate strings from the template for translation or spell checking is very difficult.
  • Validations happen only when the user explicitly asks for a validation – e.g. by clicking a button. They do not get incremental notifications.
  • Total amount of script is greater, and the script is more complex

Best Practise

Here is a sample form where:

  • Validation logic is placed in field validation scripts
  • Invalid fields are highlighted with a red border
  • No message boxes appear during data entry
  • There is a button that checks overall form validation status and if there are errors, places the user in the first invalid field

The solution has 3 parts:

  1. Global Script Objects
  2. Field validation logic
  3. Validation button

Global Scripts

There are six global scripts under the utilities script object. The ones that the form author should be aware of:

setStatus(vField, vStatus)

Called by field validation logic to handle the validation logic for a field.

highlightField(vObject)

Changes the appearance of a field (or exclusion group) to indicate that it is invalid. In this case we change the border color. Anyone who wants to re-use this mechanism can modify this script to get the visual effect they prefer.  Note that for this sample, the highlighting works best if fields do not have a raised border.  It also works best for dynamic forms.  It should be re-specified if used for a static form.

unhighlightField(vObject)

Reverts the field to the appearance defined in the template. If you modified code in highlightField(), you need to make the corresponding change in this function.

There are a three more scripts that are used internally:

registerFieldStatus(vField, vStatus)

Saves a list of invalid fields in a form variable.

getMessage(vObject)

Retrieves the validation message from a field object.

findTemplateField(vField)

Finds the template field that we use to reset visual properties.

Field Definition

Several fields on the sample form have validation logic. The general form of the validation script looks like this:

expenseReport.#subform[0].expenses.expense.amount::validate – (JavaScript, client)

// place field validation logic in the getStatus() method.

// Make sure it returns true (valid) or false (invalid).

function getStatus()
{     // place field validation logic here.

}
utilities.setStatus(this, getStatus()); // register valid/invalid state

Note the properties of this script:

  • The validation will fire every time the field (or any of its dependencies) change
  • Since the setStatus() function always returns true, the field is always valid according to the XFA processor.
  • The setStatus() function does the work to change appearances etc.

Other than the field validation script, the important aspect of this design is that the validation message is specified at the field level.

Validation Button

The validation button will:

  • Check the form variable holding the list of invalid fields
  • If there are invalid fields, extract the validation message from the first invalid field and issue a message box
  • Set focus to the first invalid field

Topic for next post: Handling mandatory fields.

Validating a Postal Code

There are several interesting scripting validation techniques that can be demonstrated in the context of a Canadian postal code field.  While the subject domain is fairly specific, the techniques used are applicable to other field validations.

The challenge is to get an accurate and user-friendly data capture experience for a postal code. Some of the considerations:

  • Make sure the field value conforms to the rules for Canadian postal codes (use regular expressions for validating)
  • Make sure that error messages are as specific as possible to assist better user input (customize validation error messages)
  • Make sure the user can optionally key in a space inside their postal code  (use an edit picture clause)
  • Make sure the postal code is entered in upper case (modify keystrokes on the change event)
  • Make sure the postal code value is consistent with other address fields: province and city (inter-field validation)

The sample form can be found here.

Canadian Postal Code Rules

The default way to validate a postal code is to use a validation picture clause: text{A9A 9A9}. However, this picture allows many illegal postal codes. For example, the meta character “A” in a picture clause allows any Unicode letter value – whereas a postal code letter is far more restricted:

  • The first character of a Canadian postal code corresponds to a geographic region and is limited to the set of characters: ABCEGHJKLMNPRSTVXY
  • The third and fifth characters may not include the letters D, F, I, O, Q or U (because they are hard for OCR software to deal with).

In order to implement these restrictions, we can use regular expressions. The JavaScript string class has a match() method that searches for a pattern.  For example, the code to validate the first character is:

if (vTestValue.charAt(0).match(/[ABCEGHJKLMNPRSTVXY]/) == null)
{
… error condition …
}

Specific Error Messages

When the user enters an invalid postal code, be as specific as possible in telling them what to fix. There is a world of difference between:

“Invalid postal code” and “Second, fourth and sixth postal characters must be numeric”

For the person who typed in a letter “O” instead of a “0” (zero), this will be the clue they need to correct their error.

To make this work, validate the portions of the postal code individually and then set the field error message accordingly:

var vNums = vTestValue.charAt(1) +
vTestValue.charAt(3) +
vTestValue.charAt(5);
if (vNums.match(/[0-9][0-9][0-9]/) == null)
{
vTestField.validationMessage =
“Second, fourth and sixth postal characters must be numeric”;
return false;
}

Note that you might choose to store error messages as form variables.  That way they will be examined by Designer’s spell checker.

The Optional Space

The user ought to be able to enter their data with or without a space – but we want to store it consistently – without the space. To accomplish this, use an edit picture clause: text{OOO OOO}

Edit picture clauses are used by Reader to:

  1. format the raw value for editing when the user enters the field
  2. parse the edited value to set the raw value when the user exits the field

For example, a raw value: A2A2A2 will be displayed as A2A 2A2 when the user enters the field. When they exit the field, both A2A2A2 and A2A 2A2 will parse into A2A2A2 successfully. Note that I used the meta character “O” (letter or digit). This is so that if the user enters invalid characters, we still parse/format the space correctly.

Naturally, we also use a display picture clause to render with the space: text{A9A 9A9}

Upper Case Only

The easiest way to force the value to upper case is to trap the characters as the user enters them and convert them as they type. We do this with a very simple change event:

form1.address.PostalCode::change – (JavaScript, client)
// Change all user entered characters to upper case.
xfa.event.change = xfa.event.change.toUpperCase();

Because we know that the input data is in upper case, it makes our validation logic cleaner, as it needs only be concerned with the upper case variations.

Consistency with City and Province

The first character of the postal code determines the geographic region.  We can assign the province based on the postal code. In a couple of cases (M and H) we can also assign the value for the city (Toronto and Montréal).

More Considerations

While we now have a better-than-average postal code validation, it does not ensure100% correctness. Since only about 12% of the possible 7.2 million postal code variations are currently allocated, it is still pretty easy to enter a non-existent postal code.

Because this validation script is as specific as possible, it does mean that the script may need to be updated as the postal code rules evolve (The Canadian postal code rules were modified after Nunavut was added in 1999).

My sources for postal code specifics were:
http://en.wikipedia.org/wiki/Canadian_postal_code and http://www.columbia.edu/kermit/postal-ca.html.

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.

Numeric Constraint: no zeros

I came across a form design problem where the form author wanted to disallow a value of zero in their numeric fields. The desired user experience is that when the user enters a zero, then on exiting the field the field reverts to empty (null) – no need to emit an error message – just do not allow the (invalid) zero entry.

Display Pattern

If the concern is about the appearance of the field only, then the easiest way to implement this functionality is with a special display pattern: zero{}.

In fact, using this solution you could also have a zero value display some other indicator.
E.g. using: zero{‘n/a’}|num{$zzzz9.99} a field with zero value will render as: “n/a”, and when it is non-zero will render as a currency amount.

However, if the form author really wants to avoid storing a zero value, then changing the display is not enough. We need to actually coerce the raw value to zero.

On-exit script

The solution the customer came up with was to add an on-exit script to each numeric field:

form1.NumericField1[0]::exit(JavaScript, client)
if (this.rawValue == 0)
    this.rawValue = null;

But this is a labor-intensive solution. It requires adding this script to every single numeric field on the form.

See the attached sample form for two alternate methods of enforcing the “no zero” constraint. The sample uses a subform calculate script to monitor (and modify) the values of all the nested fields. The form uses two different methods for setting the values to zero: a recursive loop and a predicate lookup.

Recursive Loop

We use a script to loop through a subform and all its dependents checking for numeric fields with a value of zero:

form1.MovingExpenses1::calculate(JavaScript, client)
numericCheck.eliminateZero(this);

form1.#variables[0].numericCheck – (JavaScript, client)
function eliminateZero(vContainer)
{
    if (vContainer.className == "field")
    {
        // The typeof() check makes sure that we don’t
        //
try this on <text> fields
        if (typeof(vContainer.rawValue) == "number" &&
                               
vContainer.rawValue == 0)
        {
            vContainer.rawValue = null; 
        }
    } else
    {
        // If this object is not a field, then it
        //
may be a subform/subformSet/area
        // If so, recursively check all descendents
        var vChildren = vContainer.nodes;
        for (var i=0; i<vChildren.length; i++)
        {
            var vChild = vChildren.item(i);
            // .isContainer returns true if the element
            // is not a property
            // If so, call ourselves recursively
            if (vChild.isContainer)
                eliminateZero(vChild)
        }
    }
}

Note especially the check to see if the field needs to be reset to null:

if (typeof(vContainer.rawValue) == "number" &&
                       
vContainer.rawValue == 0)
{

    vContainer.rawValue = null;
}

Text fields will not be impacted by the check. In the sample, you could type “0” into the “Method of travel” field, and it will not be reset to null.

Using the recursive script has the best backward compatible story. It worked for me in Reader 7.1, while the predicate version worked for me in Reader 8.1.

Performance

Since this script is recursive, it may be tempting to simply place it under your root subform. This way you need only one instance of the script in your whole form. For small forms, this is fine. But consider what is happening here: Every time a user modifies a field value, this script fires and traverses the entire form, examining each field. The performance of the script will be inversely proportionate to the size of your form. Sooner or later you will reach a form size where performance starts to become an issue. The alternative is to embed the script in subforms in lower level sections of your form.

Predicate lookup

If your subform does not have any nested subforms, then you can perform a more terse, predicate-based check:

Some of you may be familiar with evaluating predicates in order to bind to “inverted” schemas. They can also be used when evaluating SOM expressions in the form DOM:

form1.MovingExpenses2::calculate(JavaScript, client)
var vZeroList = xfa.resolveNodes("$.#field.(typeof(rawValue) == ‘number’ && rawValue == 0)");
for (var i=0; i<vZeroList.length; i++)
      vZeroList.item(i).rawValue = null;

Breaking down the SOM expression we passed to resolveNodes() :

$.#field.(typeof(rawValue) == ‘number’ && rawValue == 0)

By the rules of predicate evaluation, this is the same as:

"for each child element with a className: ‘field’, evaluate this script:
typeof(rawValue) == ‘number’ && rawValue == 0
If the evaluation returns true (1) then add the candidate field element to the result set. "

Ok, so clearly the predicate version does not really have any clear advantage over the recursive loop; it was just a feeble excuse to introduce the topic of predicate expressions.

There is a lot that could be said about predicates, but that is a topic for another blog entry…