Posts in Category "validations"

Script Validation vs. Null Validation

There is one question I get asked frequently enough that it warrants its own blog entry.

Someone tries a validation script that looks like this:

this.rawValue !== null;

But the validation never fails.  In spite of the fact that the field is null, this field always reports back that it is valid.

The reason is because of this rule: "empty/null fields never fail a script validation".  The reason behind the rule is that we want to allow designers to define validations without worrying about null as a special case.  Take an example:  the user needs to enter a value in a ‘Age’ field where the value must be greater than 18.  The way to express this is with this validation on the Age field:

this.rawValue > 18;

In an empty field this evaluates as "null > 18;" — which evaluates to false.  Until the user enters a value, this validation script will always return false.

But allowing a script validation to fail on an empty field would be a lousy user experience.  As soon as they open a blank form they would get a pile of invalid fields — even before beginning to enter data.  What should a form designer do?  They could change their script to explicitly allow null:

(this.rawValue === null) || (this.rawValue > 18);

But now there’s the problem that we really do want to make sure they enter a value.  i.e. the experience we want is that the field starts out in a valid state.  Don’t complain about script validations until they’ve actually entered a value in the field — but also don’t let them submit their data until the field has been populated with a value!

Of course, the answer is to make the field mandatory (the null test validation).  By making the Age field mandatory we know that the user will not be hassled as soon as they open the form; but they also won’t be allowed to submit until they’ve provided a value.  And the form author doesn’t have to handle a null value as a special case.

—–

Gone Again

I see Merriam-Webster has added "Staycation" to their dictionary.  Sounds good to me.  I will be away for another week at the family cottage.  I’ll be be back to respond to comments on the 27th or 28th.

Layout Methods to Find Page Positions

This post will be in the deep end for many of you.  But for those who design forms that extract position information from the xfa.layout object, you might well find some answers to longstanding problems — especially if you make use of multiple content areas on your master pages.

This blog entry deals specifically with the functions:
xfa.layout.x(), xfa.layout.y(), xfa.layout.h(), xfa.layout.w().

Return Value

The description for layout.x() says: "Determines the x coordinate of a given form design object".  This is true.  Incomplete, but true.  In fact, the x() and y() methods return a coordinate relative to the parent of the object.  The offset of the root subform is relative to the content area.  To find an absolute page coordinate, you could (in theory) traverse up the hierarchy and add the x coordinate of each ancestor plus the x coordinate of the content area.  But the APIs don’t make this easy.  First of all, it’s not obvious which content area contains your object.  And if your ancestor subform originated on a different page, it will have a different offset parameter.  Finding an absolute page position is complicated.  But not impossible.

Parameters

The layout position methods all take the same set of parameters.
e.g. xfa.layout.x(<object>, <units>, <offset>);

object is self evident.  You pass in a reference to some container (field, subform, draw etc).

units is also easy. "in" will cause the function to return a numeric value representing inches.

offset is … complicated.  The documentation for xfa.layout.x() says:

"An integer representing the number of pages to offset the x coordinate of the object, beginning with the first page the object occurs on. If left blank, the default value is 0."

This is partially true.  It is true only in the case where there is one content area per page.  The full truth is more complicated.  The documentation for xfa.layout.h() says:

"An integer representing the amount to offset the height value of a form design object, beginning with the first page the object occurs on. If left blank, the default value is 0."

Hmm.  Not true at all.  It’s not an offset of the height value.  Someone forgot to copy/paste the part of the description that says "number of pages".  We’ll work toward a more accurate description in the next section…

Multiple Content Areas

For forms with multiple content areas, here is the actual behaviour of the offset parameter:

The offset parameter indicates which relative content area the object appears in.  e.g. If a subform spans 2 pages and 5 content areas, you can call xfa.layout.h with offset values of 0, 1, 2, 3, 4 to return the the height of the object in each of the 5 content areas. 

This is great but, now the problem becomes: how do we find out which actual content area the object is in?  You can’t assume that the object appears in each content area on a page.  A subform with an explicit overflow target might skip a content area on any given page.

Fortunately, there is a heuristic that allows us to infer the content area in which an object appears. 
When you make a call to: xfa.layout.pageContent(0, "", false); you get a list of all objects on a page — including the content area objects.  If a field or subform appears in more than one content area on a page, that object will appear multiple times in the returned list.  The list appears in layout-order. Content area objects appear before the objects that have been placed in that content area.  To find out which content area an object appears in, look back up the list to find the most recent content area.

Finding Absolute Coordinates

Finding the x coordinate of an object involves adding the x coordinates of all the ancestors plus the x coordinate of the content area.  But, as mentioned above, the ancestor subforms may have originated in a different content area.  In order to add up the x coordinates, you have to find out the offset value of your parent object in this content area.  The script code to figure this all out is pretty complex.  I’ve put all the complexity under a script object that you can call:

// This function returns an array of extents for a given object
// There will be one entry in the array for each content area
// an object appears in.
// Each entry is an object with these properties:
//   Extent.page
//   Extent.contentArea  (SOM expression)
//   Extent.x            (absolute page position)
//   Extent.y
//   Extent.w
//   Extent.h  
// All measurements are inches.
function getExtents(vObject)

I have attached a sample form with multiple pages, multiple content areas, and which displays the results of querying layout positions.

Application

Once you’re able to find page positions, how can you use that information? Placing transpromo content is the best example.  I have also developed another sample where I used a different kind of field highlighting.  For each field error, the form places a "highlighter" subform on the master page (the highlighter is an arrow).  When you open the form, click on the "highlight errors" button.

I have another purpose in mind, but that’s the topic for a future blog post…

XFA 3.0: List of Invalid Fields

This post describes the last of the enhancements targeting form authoring usability in XFA 3.0/Acrobat 9.1.

The subform object has a new script method:

<container list> = subform.getInvalidObjects();

This function returns a list of all the descendant containers (field, exclGroup, subform) of a subform which are in an invalid state.  If the subform that this script method is called on is itself invalid, that subform will be included in the returned list. The list is only generated on demand by recursively traversing the subform. The list returned is in document order.

This function will allow form authors to selectively highlight errors on sections of their form.  For example, in the "exit" event of a subform, you could highlight the missing or invalid fields contained within that subform, and direct the form filler to complete that section before continuing with the rest of the form.

As mentioned in yesterday’s post, missing mandatory fields are not considered invalid until they have been edited or there has been a call to execValidate().  To be consistent, the call to getInvalidObjects() respects the same rule.  The returned list will not include missing mandatory fields until the XFA processor considers them invalid. 

One of the places where the list of invalid fields is useful is during the submit process.  Form authors can use the preSubmit event to detect invalid fields, notify the user and cancel the submit action.  Note that submit performs a call to execValidate() — but only after preSubmit has fired.  This means that a call to getInvalidObjects() in the preSubmit event might not include missing mandatory fields.  If you want to be sure those fields included, simply call execValidate() before the call to getInvalidObjects().

It will be some time before form authors can really take advantage of the XFA 3.0 enhancements — mainly because it will be a while before Acrobat/Reader 9.1 is on enough desktops.  In the meantime, I expect authors will continue to make use of JavaScript frameworks to gain enough control over the user experience.  If you are new to the idea of JavaScript frameworks, have a look at the one that I’ve included in various samples.  The framework is described in a series of posts: Validation Patterns: Part 1, Part 2, Part 3 and here. The most recent version of the framework is embedded in the samples for this post

The framework was designed to:

  • Imitate the "form authoring usability" enhancements described in the last few XFA 3.0 blog postings
  • Leverage as much of the native XFA validation mechanism as possible for controlling validations
  • Over time, be replaced by the new built-in XFA 3.0 capabilities

I anticipate one one more post on XFA 3.0 enhancements and then back to regular programming.

XFA 3.0: Validation State Change Event

The field, exclGroup and subform elements have a new event: validationState.  The primary use of this event is to change the appearance of fields as they become valid or invalid.

Just for review — there are three ways in which objects can be considered invalid:

  1. A mandatory field/exclusion group that does not have a value
  2. A field with a value that cannot be formatted with its validation picture format
  3. A field/subform/exclusion group whose validation script returns "false"

Rules controlling the validationState Event

The validationState event fires under the following conditions:

  • Immediately following the initialize event — allowing the form author to control the initial appearance of the object
  • When the field/subform/exclusion group transitions between a valid and invalid state (or vice versa).
  • When the reason for the invalid state changes e.g. a field is in an invalid state because it is not filled in (missing mandatory).  The user then fills in a value that violates the validation picture clause.  In this case the field goes from one invalid state to a different invalid state and the event will fire.
  • The form session begins tracking mandatory fields

The last point needs a bit more explanation.  When a field is marked as mandatory, it starts the form session in a valid state — even though it does not have a value.  The reason is that we don’t want users notified about a whole bunch of invalid empty fields as soon as they open a blank form.  There are 3 things that will put a missing mandatory field into an invalid state:

  1. A populated mandatory field gets emptied
  2. A call to execValidate() on the field or one of its ancestors
  3. A submit action (which is really the same as an execValidate())

When any one of these three operations happen, the validationState event will fire for any unpopulated mandatory fields.

To be on the safe side, script associated with the validationState event should be robust enough to allow the event to fire multiple times — even if the validation state hasn’t actually changed.

The new errorText property

So the validationState event has fired… how does the script author know whether the field is now in a valid or invalid state?  Authors can check the new property on field/subform/exclusion group: errorText.  When a field is in a valid state, this property will return an empty string.  When in an invalid state, it will return the user-defined message for that error. 

Objects may have 3 different user-defined messages — one for each kind of validation failure.  These messages are represented in the scripting model as:

field.validate.message.scriptTest.value
field.validate.message.formatTest.value
field.validate.message.nullTest.value

or via their shortcut properties:

field.validationMessage
field.formatMessage
field.mandatoryMessage

The errorText property will be populated with the message from the associated validation failure.  If the user has not defined a message, a default message is provided ("Validation failed.").

Turning off Acrobat/Reader Field Highlighting

If you are using the validationState event to change the appearance of fields, the automatic field highlighting of Acrobat/Reader can get in the way.  Fortunately, it can be turned off.  This script (placed on a form:ready event) will do the trick:

    if (xfa.host.name == "Acrobat")
       app.runtimeHighlight = false;

Example

This example will hide the border on valid fields and for invalid fields will colour the border red.  Note that the example places the script on a subform and uses event propagation to cause it to apply to all nested objects.

<subform>
  <event activity="validationState" listen="refAndDescendents">
    <script>
      if (HasValue($event.target.errorText)) then
        $event.target.edge.presence = "hidden";
      else
        $event.target.edge.presence = "visible";
        $event.target.borderColor = "255,0,0"
      endif
    </script>
  </event>
</subform>

 

XFA 3.0: Message Handling Options

Of the XFA 3.0 enhancements, it’s safe to say this one was overdue.  Today XFA processor in Acrobat/Reader notifies users about invalid fields by popping up dialog boxes.  However, form authors feel so strongly about wanting control over the notification mechanism that they develop elaborate JavaScript frameworks to avoid using the message boxes associated with the built-in validation/notification mechanism.

To put the flexibility back in the hands of the form author, XFA 3.0 has a new configuration setting to control how errors are handled:

<acrobat>
  <common>
    <validationMessaging>
        noMessages              |
       
firstMessageOnly        |
        allMessagesIndividually |
        allMessagesTogether

    </validationMessaging>
  <common>
</acrobat>

Don’t worry about remembering the syntax or where it goes — Designer will have a UI for setting this option.
The impact of this setting is pretty self-explanatory — but I’ll explain anyway:

  • noMessages – Acrobat/Reader will not emit any messages for invalid fields.  It will be up to the form author to notify the user in some way — likely by changing the appearance of the invalid fields.

  • firstMessageOnly – When there are multiple errors on the form, Acrobat/Reader will display a message box for the first error and will suppress messages for all subsequent errors.  This prevents the user from being inundated with a long series of messages — one from each invalid field.

  • allMessagesIndividually — (default) This corresponds to today’s behaviour.  Well, not quite.  There are instances today where we combine messages — for example messages for missing mandatory fields.  But combined messages don’t happen as much as you might like.

  • allMessagesTogether — Aggregate all the messages from all invalid fields and emit a single dialog with all messages combined.

The sequence of validation script execution remains the same no matter which messaging option is chosen.  If the user attempts to submit when there are still missing or invalid fields, the submit operation will cancel with a message box.

XFA 3.0: presence="inactive"

This is the first in a short series of posts to describe the new functionality found in XFA 3.0.

If you have developed a form with even modest complexity, you have almost certainly manipulated the presence property of fields, subforms and exclusion groups.  With XFA 3.0 we have extended the possible values of this property to include "inactive".

The existing presence attribute can be one of these properties:

  • visible – (default) Object is visible
  • invisible – Object is invisible, but still participates in layout and event processing.  i.e. space is allocated in the layout to display this object, its calculations and validations still fire.
  • hidden – Object is invisible and excluded from layout.  No space is reserved for this object. Its calculations and validations still fire.

In XFA 3.0 we now add:

  • inactive – Object is hidden and excluded from event processing.  Calculations, validations and other events do not fire.

Scenario

A mortgage application form is sent to a client. The form has an optional section where the user may apply for mortgage life insurance.  The life insurance application section is pre-populated with client information from the server.  If the user chooses not to apply for life insurance:

  • the optional section will remain hidden
  • The calculation and validation scripts in the optional section will not fire
  • Other event scripts such as layout:ready, form:ready, prePrint, preSubmit etc. do not fire
  • We do not check for mandatory fields

All these behaviours will be true as long as the subform that defines the life insurance application is set to presence="inactive".

Once the user checks the box to apply for insurance, we toggle the setting to presence="visible" and then that section of the form becomes fully functional.

Containers, Properties

The new behaviour for presence applies only to these container objects: subform, field, exclGroup.  The presence attribute is also found on draw, fill, items, edge, border, caption and corner — however in these contexts it will not introduce any new behaviour.  It will behave the same as "hidden".

The Deep End

In a previous post, I described the various stages of processing we go through when we open a form:

  1. Loading content
  2. Merging data with template (create Form DOM)
  3. Executing calculations and validations
  4. Layout (pagination)
  5. Render

The enumeration of the presence attribute determines which of these stages a form object will participate in:

  1. Loading content
  2. Merging data with template (create Form DOM) (visible, invisible, hidden, inactive)
  3. Executing calculations and validations (visible, invisible, hidden)
  4. Layout (pagination) (visible, invisible)
  5. Render (visible)

A Form to Design a Form

There are a class of form designs where the form fill-in experience follows a basic pattern with a small number of variations.  A survey form is the prime example.  The variable pieces are the question text and a set of response types.  A user who wants to create one of these forms should not have to learn a complicated form design tool.  Given the relatively small number of properties that need to be specified to make a survey work, it should be possible to design a single dynamic form that can be shaped by any survey definition — i.e. one form that can render all variations of a survey.

We can design that survey form, but then we need to figure out an easy way for the author to define their survey.  This is really just another data capture problem — one that can be handled by a form.  So we use a form to design our survey.  A form to design a form.  Kind of like a play within a play.

To accomplish the form-within-a-form, there are two sets of data.  The survey-designer-form captures information for question text and response types and saves this information as its form data.  The fill-in-form has built-in variability (dynamic subforms) whose appearance and behaviour are customized from the data produced by the designer form.  The design data moves from being form data for the designer form to become metadata for the fill-in form.  When the fill-in version of the form runs, it produces its own data — the survey results.

Two Forms in One

Ideally, design and fill would be two separate forms.  But two separate forms means moving data between forms.  And even a relatively simple operation such as moving data is probably more than we can expect from our target users’ skill set.  As well, any multi-step process gets in the way of quickly previewing a survey.  To keep the experience as simple as possible, I’ve taken the approach of combining the design and fill-in experience into the same PDF.   The advantage is that the user deals with only one form and doesn’t have to manage their data.  There are likely better ways to do this.  If the experience were tethered to a server (LiveCycle for example :), it would be easier to manage the data on behalf of the user and keep the forms separate.  That would also make it easier to use a cool flash UI for the survey-design piece. 

But for now, to make the sample easy to distribute, I’ve combined them into one PDF.

Here’s the sample in several stages of processing:

An XFA form can fairly easily house two separate design experiences.  In my example, I had two optional top-level subforms: DesignSurvey and FillSurvey.  During survey design, the DesignSurvey subform exists.  During preview, DesignSurvey and FillSurvey both exist.  During fill-in, only the FillSurvey subform exists.  Which subform(s) appear is controlled by the form data and by script logic.

The Design mode allows you to create sections and question within sections.  The data to define a simple one-question survey looks like this:

<DefineSurvey> 
  <SurveyTitle>A survey about surveys</SurveyTitle>

  <ChoiceList>
    <ChoiceName>YesNo</ChoiceName>
    <Choice>
      <ChoiceText>Yes</ChoiceText>
      <Value>1</Value>
    </Choice>
    <Choice>
      <ChoiceText>No</ChoiceText>
      <Value>0</Value>
    </Choice>
  </ChoiceList>

  <Section>
    <SectionName>Personal Information</SectionName>

    <Question>
      <QuestionNumber>3</QuestionNumber>
      <QuestionText>Are you married?</QuestionText>
      <QuestionType>multipleChoice</QuestionType>
      <Required>0</Required>
      <ChoiceListName>YesNo</ChoiceListName>
      <MinSelections>0</MinSelections>
      <MaxSelections>1</MaxSelections>
    </Question>

  </Section>
</DefineSurvey

When designing the form, this data resides in the normal place for form data under:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
   <xfa:data>
      <DefineSurvey>…</DefineSurvey>
   </xfa:data>
</xfa:datasets>

When we switch to "fill-mode", we move the form definition (<DefineSurvey>) to a separate dataset and the fill-in data then lives under <xfa:data>:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
  <DefineSurvey>…</DefineSurvey>
  <xfa:data>
    <Survey>
      <Section>
        <Question>
          <QuestionNumber>3</QuestionNumber>
          <QuestionText>Are you married?</QuestionText>
          <Answer>1</Answer>
        </Question>
      </Section>
    </Survey>
  </xfa:data>
</xfa:datasets>

Once the form is in "fill mode", the PDF can be distributed to users.  Enable usage rights so they can save the results.  Or better yet, host your survey on acrobat.com.

Next Steps

The form design could expose more options. e.g. conditional logic, more response types, more constraints on responses, styling options.  It’s all just SMOP (small matter of programming). 

Submit

I added a submit button to the form in order to ret
urn the survey results.  There are a couple of things that are interesting about the handling of the submission. The survey definition includes a target email address.  The submit button gets updated with target and subject with this bit of code:

var vEmailTarget = "mailto:" + xfa.datasets.DefineSurvey.Email.value
                             +
"?subject=" + xfa.datasets.DefineSurvey.SurveyTitle.value;
EmailSubmit.event__click.submit.target = vEmailTarget;

The other thing I did with submit was make use of the new event cancel capability.  When the user clicks on the "submit" button, the pre-submit event fires.  I put this script there:

Survey.FillSurvey.#subform[2].EmailSubmit::preSubmit:form – (JavaScript, client)
if (scValidate.formHasErrors())
{
    scValidate.showErrors();
    xfa.host.messageBox("The survey is incomplete.  Please fill in the highlighted questions.");
    xfa.event.cancelAction = true;
    xfa.host.setFocus(scValidate.getFirstError());
}

The xfa.event.cancelAction property is new in Acrobat/Reader 9.  It allows you to cancel the upcoming action in prePrint, preSubmit, preExecute, preOpen, preSign events.

Validation Framework

The form makes extensive use of the validation framework I defined in previous blog entries — most notably, the exclusion group objects.  The framework is contained in the three script objects at the top of the form: scValidation, scGeneral and scGroup. These are re-usable objects that can be used in forms where you want fine-tuned control over the form validation experience.

For those who have used previous versions of this framework, I added some enhancements to suit my needs for this sample:

New function: scValidate.hideErrors()

After this is called, any fields with errors are not highlighted until…

New function: scValidate.showErrors(subform)

This function causes error fields that are descendents of the input subform to be highlighted.  If subform is omitted, errors are displayed on the entire form.

New function: getFirstError(subform)

Returns the first descendent field under the given subform that has an error.  It subform is not specified, returns the first error field on the whole form.

scGeneral.assignRichText(targetField, source)

Where targetField is a rich text field and the source is either a dataValue with rich text or another rich text field.

I also changed the code that highlights invalid fields.  Instead of mucking with borders, I simply set the fill colour.

Calling FormCalc Functions From JavaScript

For form calculations, validations and general scripting problems, our form designers can choose between using FormCalc and JavaScript. FormCalc was designed as an expression grammar targeting forms functionality — and designed to be easy to use for a novice form author familiar with expressions in Excel.  JavaScript on the other hand, is a much more general purpose and powerful scripting language.  Over time, form authors have gravitated more toward JavaScript, mainly because of its familiarity.  However, there are functionality gaps — specifically a set of built in functions that are available in FormCalc but not in JavaScript. 

Many of the built-in FormCalc functions can be easily mimicked in JavaScript.  It doesn’t take a rocket scientist to write a JavaScript script object function to imitate the FormCalc sum() function. However, there are some functions that are not so easily mimicked.  The most notable of these are the Parse() and Format() functions available in FormCalc.  Parse() and Format() are the entry points into the very rich picture clause processing functionality.  When you consider the inherent locale handling, date/time functionality, different calendars etc. it’s plain to see that you don’t want to do this in JavaScript.

But now we have a problem.  Many users are committed to JavaScript because they built frameworks for controlling validations and other form behaviours.  I did the same in the series of blog posts on validations (the most recent version of the framework was in the sample from this post).  The problem is that you cannot directly call FormCalc from JavaScript.  So it would seem that you can’t enjoy both the power and familiarity of JavaScript as well as the built-in functions of FormCalc.  Well, … actually you can.

There are two design patterns I’d like to cover:

  1. Validating fields with picture clauses
  2. General mechanism for calling FormCalc functions

Validating Fields with Picture Formats

Picture clause validations are a simple, declarative mechanism for form authors to ensure that input data complies with a specific format.  If the field value can successfully be formatted with the validation picture clause, then the field is considered valid.  e.g. if your validation picture clause is "Date{YYYYMMDD}", then the field is considered valid only if its value can be formatted as a date.  If you were to express this validation as a script, you could write this FormCalc expression:

form1.#subform[0].DateTimeField1::validate – (FormCalc, client)
format("YYYYMMDD", $) <> ""

Now the question is how to tap into this functionality from JavaScript.  The short answer:

  1. Define a *display* picture format (do not a validation picture format)
  2. Write a JavaScript validation script that returns true when the field.rawValue is different from field.formattedValue

Most often when you use a validation picture clause you also use a display picture.  In fact, there’s really no reason why these picture clauses need to be different.  Combine that knowledge with an understanding of how the field.formattedValue property works:  when a field value can be formatted using the display picture, field.formattedValue will return the result of the format operation.  If the format operation fails, field.formattedValue returns the same as field.rawValue.  So to find out if a field was formatted correctly, use this JavaScript validation:

form1.#subform[0].DateTimeField1::validate – (JavaScript, client)
this.rawValue != this.formattedValue;

Mechanism for Calling FormCalc Functions from JavaScript

The solution is to use the execCalculate() method to indirectly cross the bridge between JavaScript and FormCalc.  When you open the attached sample, you will find a subform called "fc" that holds a script object called "func".  "func" has a series of embedded JavaScript functions that mimic the FormCalc functions with the same name.  Each function populates form variables with the function name and input parameters.  It then calls execCalculate() on the fc subform and returns the resulting value:

FCfromJS.fc.#variables[0].func – (JavaScript, client)
function Format(vPicture, vValue)
{
    F.value = "Format";
    P1.value = vPicture;
    P2.value = vValue;
    this.execCalculate();
    return ResultString.value;
}

The subform calculation script looks like this:

 FCfromJS.fc::calculate – (FormCalc, client)
; execute the requested function based on the input
; request parameters
if (F == "WordNum") then
    ResultString = WordNum(P1)

elseif (F == "Parse") then
    ResultString = Parse(P1, P2)

elseif (F == "Format") then
    ResultString = Format(P1, P2)

elseif (F == "Uuid") then
    ResultString = Uuid(P1)

elseif (F == "UnitValue") then
    ResultString = UnitValue(P1, P2)

else
    ResultString = ""
endif

The field that wants to use the format() functionality has a simple calculate script:

FCfromJS.#subform[1].format.Result::calculate – (JavaScript, client)
fc.func.Format(PictureClause.rawValue, Value.rawValue);

This fc subform can easily be incorporated as a custom library object in Designer that can be dragged onto any form.  It should be pretty easy to follow the design pattern if you want to extend the sample and add other FormCalc functions.

One usage note — if you want to call these functions from initialization scripts, then be sure to place the fc subform at the beginning of your template.  This is necessary because the fc subform has an initialization script that creates the necessary form variables.  By placing this subform at the top of the form hierarchy, we’ll be certain that the fc initialization event fires before other initialization events.

August 12, 2009 Update

I have updated the sample form:

  • Added access to the formcalc Get() function. 
  • Added a version checking mechanism (see this post for details).
  • The subform housing this functionality is now available as a downloadable fragment: scFormCalc.xdp
  • The functionality is accessed using the subform "enter" event rather than the calculate.  The calculate event was introducing unwanted calculation dependencies.

Canadian/US Address Data Capture

When I fill out an interactive form that prompts for an address that could be Canadian or US; I am constantly disappointed with the data capture experience. Usually the form uses a single field to capture both state and province: State/Province:________ with a drop down that lists all states and provinces. And then a single field to capture both zip code and postal code: Zip/postal Code:__________ . Or worse, the captions on the field are biased toward a US address, but allow you to enter values for a Canadian address. I.e. you get prompted for a zip code, but it allows you to type in a postal code.

The exercise for this blog entry is to come up with a data entry experience that is tailored according to country. The samples build on the work from the previous blog entry that dealt with validating a Canadian postal code.

Single Schema

The premise of the exercise is that you want to have only one place in your data where you store an address – whether Canadian or US. The samples are based on generating this XML data:

<Address>
   <Country/>
   <City/>
   <StateProv/>
   <PostZip/>
</Address>

Validate a zip code

To be fair, I thought I should try to offer advanced validation for Zip codes.  After all, I did a whole blog entry on Canadian postal codes.  No offence to my American friends, but zip codes are not nearly as interesting as postal codes. When I poked around to see if I could do more advanced validation beyond the standard Zip or Zip+4, I was pretty disappointed. The only thing I found was that there is a correlation between the first digit and the state. For example, for zip codes starting with a “1”, the state must be one of: Delaware, New York or Pennsylvania. Better than nothing. The sample forms have a utility function to validate a zip code:

/**
* validateZipCode() – validate whether a field holds a valid zip code
* @param vZip — the zip code field. If the validation fails, this field
* will be modified with an updated validation message
* @param vState (optional)– a corresponding field value holding the
* state abbreviation.  This method will make sure the first digit of
* the zip code is valid for this state.
* @return boolean — true if valid, false if not.
*/

Keystroke validation

For the Canadian postal code validation, I introduced a change event that forced the entry to upper case. This time around, I have extended that concept to disallow keystrokes that would cause an invalid zip or postal code. A few words of explanation about some of the xfa.event properties that were used:

  • xfa.event.change – represents the delta between the previous change and the current one. Usually this is the content of a single keystroke. However it can also contain the contents of the clipboard from a paste operation. This property can be updated in the change event script to modify the user’s input. It can be set to an empty string to swallow/disallow user input.
  • xfa.event.newText – represents what the field contents will look like after the changes have been applied. Modifying this property has no effect.
  • xfa.event.selEnd – The end position of the changed text. Usually when the user is typing, we are positioned at the end of string, but the user could be inserting characters at any position.

Here is the change event script for the zip code:

Address.Address.USAddress.Zip::change – (JavaScript, client)
// restrict entry to digits and a dash
if (xfa.event.change.match(/[0-9\-]/) == null)
    xfa.event.change = "";

// Allow the hyphen at the 6th character only
if (xfa.event.change == "-" && xfa.event.selEnd != 5)
    xfa.event.change = "";

// If the 6th character is a digit, and they’re typing at the end, insert the hyphen
if (xfa.event.change.match(/[0-9]/) != null &&
    xfa.event.newText.length == 6 &&
    xfa.event.selEnd == 5) 
{

    xfa.event.change = "-" + xfa.event.change;
}

var vMax = 9;
if (xfa.event.newText.indexOf("-") != -1)
    vMax = 10;

// don’t allow any characters past 9 (10 with a hyphen)
if (xfa.event.newText.length > vMax)
    xfa.event.change = "";

In hindsight, I could have done a better job with this script.  It is still possible to enter invalid data.  e.g. after adding the hyphen at the 6th character, the user could cursor back and insert digits, forcing the hyphen beyond the 6th character.  A better approach might be to modify the validateZipCode() method so that it will validate partial zip codes.  Then block any user input that doesn’t result in a correct partial zip code.

There is a similar block of logic for the postal code change event.

Customizing Data Capture

The really hard part of this data capture problem is how to tailor the input by country. I have two samples that take different approaches.

Sample 1: Different subforms for each country

In this sample, the strategy is to use two subforms for data capture. One subform that has a province and postal code field for Canadian addresses. One that is tailored for capturing a US address.

To make this design work, we create two subforms (CanadianAddress and USAddress), set the binding of each subform to “none”. Then bind the individual fields to the address data. The reason for this approach is that we want both subforms to populate the same data. Multiple fields are allowed to bind to the same XML data element, but you cannot bind multiple subforms to the same XML data.

Show/hide logic. It is not enough to simply set the presence of the subforms to visible/invisible. A hidden field will still run its validation script. We want to make the subforms optional and add/remove them as appropriate. To make this exercise a little more interesting, I assumed that we were not in a flowed layout. Now the problem is that unless you’re in a flowed context, Designer does not allow you to make the subform optional (under the binding tab). However, the XFA runtime does not have this restriction. There are two workarounds: 1) Modify the source in the XML view 2) fix it in script. I chose the latter approach. Subform occurrences are managed by the <occur> element. By default, the address subforms will be defined as:

<occur initial="1" m
ax="1" min="1"/>

We can change the minimum via script in order to make them optional:

Address.Address.Country::initialize – (JavaScript, client)
USAddress.occur.min = "0";
CanadianAddress.occur.min = "0";

Once the subforms are defined, simply place them on top of each other at the same page location. When changing country from the country drop down list, the subforms will toggle on/off accordingly:

Address.Address.Country::validate – (JavaScript, client)
// Choose which subform address block to use depending on the country
_USAddress.setInstances(this.rawValue == "U.S." ? 1 : 0);
_CanadianAddress.setInstances(this.rawValue == "Canada" ? 1 : 0);
true;

Sample 2: One Subform, change the field properties

In this sample, the strategy is to create one set of dual-purpose fields. One field to capture either a postal code or a zip code and one field to capture either a state or a province. When the country changes, we modify the field definitions so that they behave appropriately. The changed properties included the caption text, the picture clauses and the contents of the state/province drop down lists. The validation that happens in the change event and in the validation script needs to branch to accommodate the appropriate context.

The logic to toggle the field definitions looks like:

Address.Address.Country::validate – (JavaScript, client)
if (this.rawValue == "U.S." && 
    ZipPostal.caption.value.text.value != "Zip Code:")
{
    ZipPostal.caption.value.text.value = "Zip Code:";
    ZipPostal.ui.picture.value = "";
    ZipPostal.format.picture.value = "";

    StateProv.caption.value.text.value = "State:";

    StateProv.clearItems();
    StateProv.addItem("Alabama", "AL");
    StateProv.addItem("Alaska", "AK");
    StateProv.addItem("Arizona", "AZ");
  . . .
    StateProv.addItem("Wyoming", "WY");
} else if (this.rawValue == "Canada" &&
           ZipPostal.caption.value.text.value = "Postal Code:"
)
{
    ZipPostal.caption.value.text.value = "Postal Code:";
    ZipPostal.format.picture.value = "text{A9A 9A9}";
    ZipPostal.ui.picture.value = "text{OOO OOO}";

    StateProv.caption.value.text.value = "Province:";

    StateProv.clearItems();
    StateProv.addItem("Alberta", "AB");
    StateProv.addItem("British Columbia", "BC");
    StateProv.addItem("Manitoba", "MB");
. . .
    StateProv.addItem("Yukon", "YT");
}
true;

Comparing the approaches

  • Both samples work in Reader version 7, 8 and 9
  • Sample 2 is easier to design, even though it requires more script.
  • Sample 1 is easier to extend in the event that you want your address block to handle more than just two countries.
  • Sample 1 requires dynamic forms.
  • Sample 2 could be modified to work for forms with fixed-pages. You would need to change the form design so that the caption is represented by a protected field (captions can be modified only on dynamic documents).

Adventures with JavaScript Objects

I have to start with an admission — I’m not a trained JavaScript programmer.  This became evident to me as I reviewed the latest version of the exclusion group sample form.  I was not happy with the performance, so I thought I would stick in a timer and see if I could measure and improve performance.  Adding the timer was no problem.  I changed the code on the subform remove button.  Note the time calculations at the beginning and end:

form1.#subform[0].NPSA3.NPTable.Row1.Cell14::click – (JavaScript, client)

var vTime1 = (new Date()).getTime(); // gets the time in milliseconds

// Removing a subform in the middle of a slie can
// change the SOM expressions of subforms later in the list
// This means that we have to clear the error list
scValidate.clearErrList();

if (_Row1.count > _Row1.min)
    _Row1.removeInstance(Row1.index);
// rebuild the error list
xfa.form.execCalculate();
xfa.form.execValidate();

var vTime2 = (new Date()).getTime();
console.println(vTime2 – vTime1);

 

For my test I added 20 subform rows.  With a total of 21 rows there are 65 exclusion groups on the form.  Deleting a row causes all 65 groups to be re-evaluated.  (actually most of them get evaluated twice, but that is a bug behavior that I cannot control).  With the measurement in place I could see that deleting one row was taking 2753 milliseconds on my laptop.  As I deleted more, the deletions sped up — since there were fewer groups to evaluate each time.  By the time I was down to two rows, the delete took 454 milliseconds.  All much too slow.

It did not take long for me to realize that my problem was the JavaScript objects I had defined to implement exclusion group behavior.  Here is where I expose my JavaScript naiveté.  I wanted to define an abstract group definition so that the implementation could be either based on a subform or on an array group.  When I set up the base class, I followed a C++ pattern.  I defined a base class with methods that the derived classes would override:

function exclusionGroup()
{
}
exclusionGroup.prototype.length = function() {return 0;}
exclusionGroup.prototype.item   = function(nItem) {return null;}
exclusionGroup.prototype.getMax = function() {return null;}
exclusionGroup.prototype.getMin = function() {return null;}

One of the derived classes:

// Implementation of exclusionGroup for a subform container
function subformGroup(vSubform)
{
    this.mSubform = vSubform;
}
subformGroup.prototype         = new exclusionGroup();

subformGroup.prototype.length  = function()        
    {return this.mSubform.nodes.length;};

subformGroup.prototype.getMax  = function()        
    {return this.mSubform.vMaxSelected.value;}

subformGroup.prototype.getMin  = function()        
    {return this.mSubform.vMinSelected.value;}

subformGroup.prototype.selectedContainer = function()
{
    if (this.mSubform.vOldSelectedContainer.value == "")
        return null;
    return this.mSubform.resolveNode(this.mSubform.vOldSelectedContainer.value);
}

What I failed to take into account is that JavaScript is completely interpreted.  e.g. When the getMax() method on the derived class gets called, it matters none that the method exists in a base class.  The interpreter simply checks the class instance to see if it has the getMax() method.  The base class was just extra processing overhead with no benefit.  The base class might have had some benefit if we had shared implementations of some methods — but we didn’t. So I removed the base class.  I created two classes: subformGroup and arrayGroup that both define the same member variables and functions.  There is no type checking.  The onus is on the programmer to make sure the property names, methods and parameters all match. 

The other revelation is that I did not need to extend the objects using prototype.  Using prototype impacts the class definition — i.e. all instances of a class.  You can extend a single instance of a class without bothering with prototype.  That seemed to improve performance as well.

In the revised form, my object definitions looked like:

// Implementation of exclusionGroup for a subform container
function subformGroup(vSubform)
{
    this.mSubform          = vSubform;
    this.len               = vSubform.nodes.length;
    this.maximum           = vSubform.vMaxSelected.value;
    this.minimum           = vSubform.vMinSelected.value;
    this.selectedContainer = function()
    {
        if (this.mSubform.vOldSelectedContainer.value == "")
            return null;
        return this.mSubform.resolveNode
                    (this.mSubform.vOldSelectedContainer.value);
    } 
    …
}

// Implementation of exclusionGroup for an array group
function arrayGroup(vHost)
{
    this.mHost             = vHost;
    this.len               = vHost.length.value; 
    this.maximum           = vHost.max.value;
    this.minimum           = vHost.min.value;
    this.selectedContainer = function()
    {
        if (this.mHost.vOldSelectedContainer.value == "")
            return null;

        return this.mHost.parent.resolveNode
                   (this.mHost.vOldSelectedContainer.value);
    }
    …
}

Simplifying the object definitions had a big impact on performance.  Deleting the 21st subform went from 2753 milliseconds to 1236 milliseconds.  Well over a 2x improvement.  Still not fast enough for my liking, but good enough for one round of performance tuning.  Now maybe I’ll go out and buy a JavaScript book…