Posts in Category "Scripting"

resolveNode vs. resolveNodes

Today I’d like to poke at a design pattern I see fairly often.  The code looks like this:

var sum = 0;
var numberOfDetails = po._detail.count;
for(var i = 0; i < numberOfDetails; i++) {
    var nodeName = "po.detail[" + i + "].subtotal";
    sum += this.resolveNode(nodeName).rawValue;
}
            

Notice how inside the loop we’re constructing a SOM expression to use for resolveNode(). The constructed SOM expression will look something like: po.detail[2].subtotal.

Let’s look at the alternative:

var sum = 0;
var details = this.resolveNodes("po.detail[*].subtotal");
for (var i = 0; i < details.length; i++) {
    sum += details.item(i).rawValue;
}

The second variation is easier to read, easier to code and will be processed more efficiently.  Why didn’t the author code it this way to start with? Likely either: a) they didn’t realize that a single SOM expression could find all their nodes or b) they didn’t realize there is both resolveNode() and resolveNodes().  And since I don’t know the reason, let’s unpack both of those topics.

SOM expression to return multiple nodes

Look again at the second SOM expression: po.detail[*].subtotal. As this SOM expression is processed, it will first find all the detail subforms under po, and then for each of those detail subforms it will check for a child named subtotal and add it to the returned list.  I suppose what’s surprising is the realization that the wildcard can go anywhere in the SOM expression.  In fact, it’s permissible to have multiple wildcard expressions.  Suppose I had a form with a list of family members who each had a list of pets.  To get a list of all the pet names, I could use this expression: family.member[*].pet[*].petName

resolveNode vs resolveNodes

The difference between these methods is in what result you’re expecting. resolveNode() expects to process a selection that returns a single node. If you pass it an expression that returns multiple nodes, it will cause an error.  The return value of resolveNode() is either a node or null.

resolveNodes() expects to return a list of nodes.  Its return value is a nodelist — which could be empty.  The nodelist has two methods for traversal: list.length (return the number of nodes in the list) and list.item(n) return the nth item.

And while I’m on the topic, there are a couple other interesting things about these methods.  They begin their search from the context of the node they’re called from.  That means the result of total.resolveNode() will be different from the result of xfa.resolveNode().  For example, suppose my form/data has the structure:

<purchaseOrder>
  <po>
    <item>
      <subtotal/>
    <item>
    <item>
      <subtotal/>
    <item>
    <item>
      <subtotal/>
    <item>
    <total/>
  </po>
</purchaseOrder>

 

For the total calculation, we want a list of all the subtotal fields.  If we anchor the search from xfa, it looks like:

xfa.resolveNodes("form.purchaseOrder.po.item[*].subtotal");

If we anchor it from within the total calculation, it looks like:

total.resolveNodes("item[*].subtotal");

The second variation is preferred.  It’s easier to read, and since it references fewer nodes, it’s more durable — less susceptible to breaking if a node gets renamed or a subform added/removed. 

By the way, Niall O’Donovan (active contributor in the forums and a regular commenter) has written a blog post that does a great job of explaining SOM expression: http://www.assuredynamics.com/index.php/2011/05/som-expressions/. Niall has a gift for explaining the basics of the technology. The post includes a very spiffy sample form that helps visualize SOM expressions. (he also constructed SOM expressions, but he’s certainly not the only one…)

The Deep End

There are a few more things that could be said to round out the picture for those of you who like to dive one layer deeper.

The .all property

The object model has a couple of properties that can be used in place of a wildcard SOM expression. The .all property will return a list of all sibling nodes that have the same name. I could also have used the .all property in my original sum calculation:

var sum = 0;
var details = po.detail.all;
for (var i = 0; i < details.length; i++) {
    sum += details.item(i).subtotal.rawValue;
}

Of course, .all is not as powerful as [*].  We can’t code po.detail.all.subtotal.  The other problem with this calculation is that it presumes there’s at least on instance of the detail subform.  If there are no instances, the script will fail.

We also have the .classAll property to return all sibling nodes of the same type. If you use .classAll on a field, you will get a list of all the sibling fields. For example po.detail.classAll is the same as po.resolveNodes("$.#subform[*]")

Evaluation in scope

A relative SOM expression will search through the entire scope to find the result. In the example: total.resolveNode("item[*].subtotal") we will search for item among the children of all the ancestors in our hierarchy.  Specifically, the search will check the children of: total, po, purchaseOrder, form and xfa.  Of course, we stop searching as soon as we find a match. In this case we’d stop at po.  If you want to prevent this hierachy search, then you can anchor the search with a reference to "$" (the current node).  In our example that would look like:

total.resolveNodes("$.parent.item[*].detail");

Rule of Thumb

If you find yourself constructing a SOM expression with string concatenation, I’d encourage you to have a second look and see if there is an easier way to get the result. I think it’s rare to need to construct an expression.  The one exception is if you’re building an expression that uses a predicate.  But other than that case there is almost always an alternative.

Propagating Events

Back to another of the 9.1 enhancements intended to help you write less code: propagating events. See here for an overview.  Again, now that we have a Design tool capable of authoring these and now that the 9.1 Reader is more common, we should have a closer look. 

For starters, here is the sample form I’ll be referencing.  As you try the form you will notice a couple of behaviors:

  • as you move your mouse over fields, they are highlighted
  • invalid fields get a thick red border

The punchline here is that there are 14 text fields on the form, but only one each of a mouseEnter, mouseExit and validationState event scripts.  These events are defined on the root subform and apply to all descendent objects.  This is a great technique for minimizing code in your form.  A highly recommended form design technique. 

But now the bad news… So far, this functionality has not been easy to get at through Designer.  In the ES2 designer there was a check box to make an event propagate.  But the script dialog didn’t allow field events to be defined in the context of a subform.  Then because of the confusion this functionality caused, the checkbox was removed from the Desginer that shipped with Acrobat 10.  This functionality should re-emerge in a future version of Designer. 

In order to make it easier to get at this functionality, I have written a macro that will add propagating events to subforms.  The UI appears like this:

After you run the macro, the event and script will be added to your subform. Now, the tricky part — to edit the script you need to do two things: 1) close/re-open the form. Yeah, macros are still beta, and Designer doesn’t pick up on the changes made by the macro. 2) on the script dialog for the subform, choose to show: "events with scripts".

One interesting thing to note is that it is valid to have two or more of the same event defined on a subform. e.g. you could have both a propagating enter event as well as a non-propagating enter event.

The Deep End

When changing the display of an object to show an error state, it’s a bit of a challenge to revert the object back to its original state.  e.g. If you change the border color or border thickness you need to know the original color and thickness in order to restore the original state of the field.

Well, not really.  You’ll see the validationState script in the sample uses a different technique to restore the original display.  The script removes the border and assist elements from the field.  To understand why this works you need to understand the relationship between the form and the template.  The template is the definition of a form you see in Designer.  At runtime, we merge the template objects with data and create form objects.  A form field is then a sparsely defined object that has a pointer back to a template field. 

When we execute script that changes the color of a border, we modify the form definition of the border and override the default border properties found in the template. Removing the border from the form field means that we have removed the override and the field then reverts to the definition found in the template.

 

Border Control

In these times. many countries are interested in border control.  Today I’m interested in controlling field borders. Did you know that fields have two borders? There is a border around the entire field (including the caption) and there’s a border around just the content part — excluding the caption. These are edited in two different places in the Designer UI.  Field border is specified under the border tab:

The border around the content portion is specified in the "Appearance" selection of object/field:

Both of these borders can be manipulated in script.  Have a look at this sample form to see what I mean. 

You might notice that the scripts in the sample form do not use the field.borderColor or field.fillColor properties. These are shortcuts — convenience syntax that simplifies the underlying property structure. And while they’re convenient, they don’t give you full control. Most notably, they control only the outer border, do not give access to the widget border. 

As you look at the script in the sample form, you will notice some interesting things:

1) script reflects syntax. 

The (simplified) XML definition of a border looks like this:

<border presence="visible | hidden">

    <edge presence="visible | hidden" thickness="measurement">
        <color value="r,g,b"/>
    </edge>  <!-- [0..4] -->

    <fill presence="visible | hidden">
      <color value="r,g,b"/>
    </fill>
</border>
            

A script that changes a border fill color (border.fill.color.value = "255,0,0";) is simply traversing the hierarchy of elements in the grammar.

2) Edge access is tricky

The syntax to get at the four edges uses the "getElement()" method, whose second parameter is the occurence number of the edge.  Note that the sample always sets all 4 edges.  This is because edge definitions are inherited.  e.g. if only one edge is specified, then modifying the single edge property impacts all four edges.  The problem is that you don’t always know how many edges have been specified, so it’s safest to set all four explicitly.

3) Show/hide with the presence property. 

You’re likely accustomed to using the presence property of fields. That same attribute applies on the components of a border:

border.presence = "hidden";      // hides entire border — all edges and fill
border.fill.presence = "hidden"; // makes the border fill transparentborder.getElement("edge", 0).presence = "hidden"; // hides the first edge

4) There’s more.

There is more to the border definitions than I’ve shown you here. Using script you an control the four corners, fill patterns, edge thickness, border margins and more.

5) There are other borders

Subforms, rectangles, draw elements and exclusion groups all have border definitions.

A better validation pattern for 9.1 forms

Today I’d like to go back again to revisit some functionality introduced in Reader 9.1 (XFA 3.0).  In 9.1 we added some enhancements specifically designed to improve the form validation user experience — and to allow validations to happen with less JavaScript code.  Specifically:

  • Message Handling options (described here)
  • The validation state change event (described here)

Prior to these enhancements, users were avoiding the use of validation scripts because they didn’t like the message boxes that popped up on every validation failure.  But now we have control over those message boxes, and, as we’ll see, there are lots of good reasons to like using validation scripts.

First things first, turn off the message boxes.  There’s a new designer dialog for this:

Designer dialog box showing a setting where the author can specify that validations do not display message boxes.

Great! now we don’t get those pesky dialogs showing up every time.  But now, of course, it’s up to you to find some other way to convey to the user that they have invalid fields in their form.  This is where the validationState event comes in.  It fires when the form opens, and it fires again every time a field validation changes from valid to invalid or vice versa.  The expected use of validationState is to change the appearance of the form to flag invalid fields. 

One more important piece of the puzzle: field.errorText. When a field is in a valid state, this text is and empty string.  When the field is invalid, it is populated with validation error text.

Take the example where we expect a numeric field to have a value greater than zero.  The validation script looks like this:

encapsulation.A1.B.C.D::validate-(JavaScript, client)
this.rawValue> 0;

The validationState event will set the border color and the toolTip text:

encapsulation.A1.B.C.D::validationState-(JavaScript, client)
this.borderColor = this.errorText? "255,0,0" : "0,0,0";
this.assist.toolTip.value = this.errorText;

Setting the toolTip value is important for visually impaired or color-blind users who won’t notice a change in border color. (There’s another topic waiting on designing a good validation experience that works with assistive technologies).

Hopefully this looks pretty clean and easy to code.  It’s important to contrast this approach with the alternative — just to make sure you’re convinced.

The Alternative

The alternative to field-by-field validation is to centralize form validation in one big script.  The big validation script then gets invoked by some user action – usually a form submit or print or validation button. Working with the same example, this script would look like:

encapsulation.validate::click-(JavaScript, client)
var field = A.B.C.D;
var bValid = field.isNull || (field.rawValue> 0);
if (bValid) {
     field.borderColor = "0,0,0";
     field.assist.toolTip.value = "";
} else {
     field.borderColor = "255,0,0";
     field.assist.toolTip.value = "The value of D must be greater than zero";
}

Here is a sample form that has both styles of validation.  And here are some reasons why you should prefer field-by-field validation:

  • Less code.  Three lines of code vs. nine. In a form with hundreds of fields, this difference becomes compounded
  • Better user experience — immediate feedback when fields become valid or invalid
  • Form processor is aware of the invalid field and will automatically prevent form submission
  • External validation scripts need to also enforce null/mandatory fields.  In the field-by-field approach, mandatory is enforced without any script.
  • Encapsulation: A term from our object-oriented design textbooks.  In this context it means that the field definition (including business logic and messages) is self-contained.  The benefits of encapsulation include:
  • Notice that the second example references the field by it’s SOM expression: A.B.C.D; There are any number of edit operations that could change that expression: moving a field, unwrapping a subform, renaming a subform or field etc.  If any of these operations happen, the script will need to be updated.  In the first example, there is no SOM expression needed and the script can remain the same through all thos edit operations.
  • Use fragments.  If you want the field to be included in a form fragment, you need to make sure that the validation logic is included with the field.  When the logic is encapsulated, this is not a problem.  When the validation logic is outside the field, it’s much harder to find ways to have the logic accompany the field.

Not Ready for 9.1?

Assuming you’re now convinced that field-by-field validation is what you want, you might still be in a situation where you can’t assume your users have 9.1.  In that case, I’d encourage you to check out some of my older blog posts that included a script framework that allowed field-by-field validation without the 9.1 enhancements.  The most recent version of that framework was included in this blog post.

Understanding Field Values

Knowing a bit more about how field values are represented in JavaScript could make a difference in how you write script.  Today I’ll give an overview of how field values are processed.   For starters, here is a sample form that will illustrate some of the points I make below.

But first, we need to be mindful that picture clauses (patterns) impact our field values.  A brief recap of the various picture clauses (patterns) we use:

  • format pattern: Used to format field data for display
  • edit pattern: Used to format field data when the user has set focus in a field and is about to edit a value.
  • data pattern: Used to format field data when it is saved to XML.
  • validation patten: Used for field validation — does not impact field value

With that background, let’s look at the properties available:

field.rawValue: the field value in its raw form. i.e. a format that has no patterns applied and a format that is consistent across locales and is suitable for calculations.

field.editValue: The field value when formatted with the edit pattern. If there is no edit pattern you will get a reasonable default — usually the rawValue, except in the case of a date field where you’ll get some locale-sensitive short date format.  If a field value cannot be formatted using the edit pattern, then field.editValue will revert to be the same as field.rawValue.

field.formattedValue: Same as field.editValue except using the display pattern.

Some miscellaneous facts about field values:

  • field.editValue and field.formattedValue always return a string. If the field is null, these will return an empty string
  • rawValue returns a JavaScript type corresponding to the kind of field.  e.g. A text field is a string, a numeric/decimal field is a number
  • when a field is empty, rawValue will always be null
  • rawValue is what will be stored in the XML by default (assuming no data pattern has been specified)
  • There are two ways to check for a null value:
    • field.rawValue === null
    • field.isNull
  • The rawValue of a date field does not have a type date.  We chose to represent dates as a string — in the form that they will be saved.  The format used for the string is YYYY-MM-DD.
  • If you use JavaScript typeof to determine what kind of a value a field has, be aware that typeof(null) returns “object”

Knowing all this, there are some implications for the code you write:

  • If you need a JavaScript Date from a date field, you can use this function:
    function ConvertDate(sDate) {
       if (sDate === null) {
          return null;
       }
       var parts = sDate.split("-");
       // Convert strings to numbers by putting them in math expressions
       // Convert month to a zero-based number
       return new Date(parts[0] * 1, parts[1] - 1, parts[2] * 1);
    };
                
  • You should never code: field.rawValue.toString()
    This will result in a JavaScript error when the field is null.
  • If you want an expression that always returns a string, never null, use field.editValue or field.formattedValue
  • I often see code in the form:
    if (field.rawValue == null || field.rawValue == “”) { … }
    This is unnecessary.  Use one of the following:
    if (field.isNull) { … }
    if (field.rawValue === null) { … }
  • If you prefer not to use a validation pattern, you can validate a field using the display picture.  Use this validation script:
    this.formattedValue !== this.rawValue;

Optional Sections of Forms

When Adobe first released Reader 9.1 I wrote a series of blog entries describing the new forms features.  But of course, at that time it would have been easy to overlook them because you didn’t have a designer that could target 9.1.  Not to mention that your user base would take some time to upgrade to Reader 9.1.

But now some time has passed and it’s a good idea to revisit some of the 9.1 features.  Today we’ll look at field.presence = “inactive”.  For background, you may want to re-read the original post here

The sample for today shows a scenario where the form requests a payment type.  If the payment type is “credit card”, we need to expose the credit card fields to fill.  Not only that, but the credit card fields need to be mandatory. With the inactive setting, this becomes a very simple script:

if (this.rawValue === "Credit Card") {
    CreditCardDetails.presence = "visible";
} else { 
    CreditCardDetails.presence = "inactive";
}
            

If you wanted to do the same without the inactive setting, the script would look something like:

if (this.rawValue === "Credit Card") {
	CreditCardDetails.presence = "visible";
	CreditCardDetails.cardExpiry.mandatory = "error";
	CreditCardDetails.cardNumber.mandatory = "error";
	CreditCardDetails.cardType.mandatory = "error";

} else { 
	CreditCardDetails.presence = "hidden";
	CreditCardDetails.cardExpiry.mandatory = "disabled";
	CreditCardDetails.cardNumber.mandatory = "disabled";
	CreditCardDetails.cardType.mandatory = "disabled";
}

And this is with a very simple scenario where the optional section consists of three fields.  Imagine if the optional section had dozens of fields with mandatory settings, validation scripts etc.

The presence=”inactive” setting was added to help reduce the amount and complexity of code authors needed to write to author forms.  If you make use of it, you should find your forms easier to code and less difficult to maintain.

Use the change event to filter keystrokes

If you’re wondering why a sudden flurry of blog posts, it’s because I spoke at the Ottawa Enterprise Developer User Group meeting last week.  I prepared a bunch of material for that presentation, and now I need to make that material generally available.

I spoke a lot about validation techniques in form design.  Today I’ll focus on using the change event to validate user input. 

User input should be validated as early as possible.  Compare the experience between:

  1. Wait until the user submits the form — then highlight all the validation errors
  2. Validate as the user exits the field
  3. Validate input as the user types into the field

The earlier the validation happens, the better the user experience. Let’s look at what is involved in validating input as the user types into the field.

The change event fires every time the user enters data into a field.  The change is normally a keystroke, but could also be a delete or a paste operation. When the change event fires, there is lots of useful information available in the xfa.event object.  I’ll describe the properties that are relevant for today’s discussion:

xfa.event.change: the contents of the data being entered.  Normally this is the keystroke.  But it could also be the contents of the paste buffer.  Or in the case where the user hits the delete or backspace keys, it is an empty string. You can modify the value of xfa.event.change in the change event.

xfa.event.selStart, xfa.event.selEnd: Tells us where the change event will happen. "sel" is short for "selection".  selStart and selEnd are character positions.  When the user has selected text, they describe the range of selected text.  When no text is selected, selEnd will be the same as selStart and text will be inserted at that position.  You can change the values of selStart and selEnd in the change event.

xfa.event.prevText: The contents of the field before the change is applied

xfa.event.fullText: What the contents of the field will be after the change is applied.

Now, some practical examples of what you can do in the change event.  Here is a sample PDF containing all the examples.

Force upper case

If you want to make sure that the contents of your field will be upper case, then modify xfa.event.change like this:

xfa.event.change = xfa.event.change.toUpperCase();

Allow only numeric characters

If you create a field that is a numeric type, then Reader/Acrobat will automatically restrict users to valid numeric input.  But suppose you’re gathering a telephone number or a credit card number. These are normally text fields that hold numbers. In this case you want to "swallow" any changes that insert non-numeric characters. This script uses a regular expression to test the change contents and cancel if necessary:

if (xfa.event.change.match(/[^0-9]/) !== null) {
    // swallow the change
    xfa.event.change = "";
    // if the user has selected a range of characters,
    // then leave the range intact by re-setting the start/end
    xfa.event.selStart = 0;
    xfa.event.selEnd = 0;
}

Visual Feedback

I once designed a form with a telephone number that accepted only digits.  I swallowed spaces, brackets, dashes and other formatting characters that the user entered.  Then I found out that a couple of the people filling in the form abandoned it because they couldn’t enter data in that field.  They needed some feedback that their keystrokes weren’t valid.  This next example temporarily sets the field border red and thick when the user enters an invalid key.  <deepEnd>The script uses the app.setTimeOut() method.  Notice that I call it from a script object.  If the return value of setTimeOut() gets garbage collected, the event will cancel. Variables declared outside script objects will be garbage collected.</deepEnd>

// If the user has entered invalid data, cancel the event and give some visual feedback
if (xfa.event.change.match(/[^0-9]/) !== null || xfa.event.fullText.length > 10) {
    // cancel the change
    xfa.event.change = "";
    xfa.event.selStart = xfa.event.selEnd = 0;
	
    // turn the border red and thick
    this.borderColor = "255,0,0";
    this.borderWidth = ".04in";
    // Turn the border back to black after one second
    var sRevert = 
        "var This = xfa.resolveNode('" + this.somExpression + "');\
        This.borderColor = '0,0,0';\
        This.borderWidth = '.02in';";
		
    helper.timer(sRevert, 1000);
}

Updated Form Report Tool

Has it been two years already?  Seems like just the other day I published a tool for summarizing form content.

I’m sure it’s been sitting on your desktop for regular use.  In my day job I’m, often asked to look at customer forms.  Or I sometimes need to go back and review one of my own form designs.  The first thing I always want to know is the "big picture" of what’s inside this form.  That’s what the summary tool is all about. You’d be surprised what you can learn from a report. I’ll hopefully point out a thing or two in follow-up blog posts.

Now that we have a macro capability in Designer (yes, yes, still in beta) it’s time to migrate this functionality from being an external tool to a Designer macro.

Here are the files you need: FormReport.pdf and FormReport.txt (rename to FormReport.js).  If you need a reminder about how to install them with Designer, then read this blog entry.

 

Debug merge and layout: Updated

Almost two years ago I published a blog post with a tool that I developed to help debug data merge and layout problems: http://blogs.adobe.com/formfeed/2009/05/debug_merge_and_layout.html

I continue to use this sample pdf on a regular basis.  It has a reserved location among my desktop icons for quick access.  But occasionally I’d run into PDF forms that cause it to hang.  So finally I took the time to find and fix the problem.  While I was at it, I took care of a couple other minor issues.  The updated form is available for download here.  I hope you find it useful.

Sample: Convert to Strict Scoping

In the past couple of months I’ve had the opportunity to help a couple of customers through migrating a form design so that it was not dependent on non-strict scoping.

First, to review what this means, you could read this blog post describing the problem.

There’s no single technique to convert a form script so that it conforms to strict scoping. But there are a couple of common patterns.  I’ll disclose these by way of an example.

I’ve designed a sample form that makes use of “expando properties” to add multi-step undo/redo functionality to individual form fields.  In this case, the form adds an expando script object to a text field.  For review purposes, the reason this is problematic is that allowing users to modify the JavaScript representations of XFA objects means that the script engine must preserve these JavaScript objects. On large forms, this means we cannot release JavaScript objects to garbage collection and memory usage climbs.

var This = this;

This.ctl = new function() {
    this.history = [This.rawValue],
    this.current = 0,
    this.startValue = null;

    this.undo = function() {
        if (this.current > 0) {
            This.rawValue = this.history[this.current-1];
            this.current--;
        }
    }
    this.redo = function() {
        if (this.current < (this.history.length - 1)) {
            this.current++;
            This.rawValue = this.history[this.current];
        }
    }
    this.enter = function() {
        this.startValue = This.rawValue;
    }
    this.exit = function() {
        if (this.startValue != This.rawValue) {
            this.current++;
            this.history.length = this.current;
            this.history[this.current] = This.rawValue;
        }
    }
};

With this code in place, the enter event can call:

this.ctl.enter();

Similarly, assuming the text field is named “Test”, the click event on an undo button can call:

Test.ctl.undo();

This is all pretty powerful, elegant stuff, and it’s tempting to adopt this form design pattern. But as I said — the side effects are a problem.

There are two possible ways to revise the form so that it works in strict scoping:

  1. Move the expando properties to script objects
  2. Re-code the logic so that it doesn’t use expando properties

Move the expando properties

The solution in this case consists of moving the logic from the field to a script object. A by-product of the move means we have to have a referencing mechanism from the script object back to the fields. (note that the strategy of moving expandos to script properties will work only in Reader version 8.1 9.0 and later)

The steps to modify this form to allow it to work under strict scoping.

1) Move the logic to a script object

In the original form, we added an instance of the control function to each field.  In our revised design, we’ll move the instances of the control function to a script object and maintain a mapping between each field and its corresponding control function.

I added a script object called “ctl” and moved the logic from the initization event there almost verbatim:

var This;

var control = function() {
    this.history = [This.rawValue],
    this.current = 0,
    this.startValue = null;

    this.undo = function() {
     . . . .
};

The difference is that in the initialization script I created an instance of the function (with the new operator) right away.  Whereas in the script object we simply declare the function and will create instances of it later.

2) Add some bookkeeping

I’ve added some bookkeeping functionality to the script object.  This “glue” code allows us to keep our original logic intact by providing a mapping from fields to the corresponding instance of the control object:

// The fieldList object is a place to store instances
// of the control function for each field
var fieldList = {};

// We need to assign unique id's to each field.
// The unique ids will index into fieldList.

// Use nUID as a global counter to assign id's
var nUID = 0;

// register a field and create an id, and an instance of control
function register(fld) {

    // We'll store the id in the field under "extras"
    var UID = fld.extras.nodes.namedItem("UID");
    if (UID === null) {
        UID = xfa.form.createNode("text", "UID");
        fld.extras.nodes.append(UID);
    }
    UID.value = "ID" + nUID.toString();
    This = fld;

    // create an instance of the control function
    fieldList[UID.value] = new control();
    nUID++;
}

// If a field is part of a subform that gets removed, remove it here also
function unregister(fld) {
	   delete fieldList[fld.extras.UID.value];
}

// Convenience methods for accessing the control functionality
// associated with each field
function getObj(field) {
   return fieldList[field.extras.UID.value];
}
function enter(field) {
    This = field;
    getObj(field).enter();
}
function exit(field) {
    This = field;
    getObj(field).exit();
}
function undo(field) {
    This = field;
    getObj(field).undo();
}
function redo(field) {
    This = field;
    getObj(field).redo();
}

3) Change the code references

The syntax of code that called methods directly on the Test field now needs to change. The initialization script of the Test field now makes a call to:

ctl.register(this);

The enter event changes to:

ctl.enter(this);

 

The click event is now:

ctl.undo(Test);

 

4) Cleanup

When the control function was hosted by the Test field, the instance was conveniently removed when the Test Field was removed.  But now this needs to be done explicitly. The code to remove the subform has a new call:

ctl.unregister(Test);
_item.removeInstance(this.parent.index)

After following the steps above, the revised form works under strict scoping.  Check it out.

Recode the Logic

But now the question needs to be asked: was this the best way to convert the form?  The process I followed was geared to keeping the control logic intact.  My assumption was that this is where the customer has invested the most and they’d prefer to keep that code intact.  But the implementation of the control function could have been implemented differently.  The history is represented as a JavaScript array, but could be re-implemented as an array of elements under <extras>.  The final solution is more elegant — but the effort to get there is riskier.

I’ve attached a 3rd variation of the form that implements undo/redo using extras. I won’t go into details on how it works here.  But one of the interesting side-effects of using extras rather than JavaScript variables is that due to formstate functionality, the undo history is preserved when the form is closed and saved. i.e. you can re-open the form later and the undo history is intact.