Posts in Category "Exclusion Group"

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.

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…

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 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

Exclusion Groups v3.0

This is the third and final post describing a pattern for building your own exclusion group.  The first two posts are required reading for this entry:
Build a better exclusion group   Exclusion Groups v2.0

The included sample builds on version 2 and adds a couple of useful pieces of functionality:

  • Allow the exclusion group to have multiple entries selected
    (specify the minimum and maximum selected entries)
  • Make the exclusion group mandatory
  • Make the sample work in Reader 7

The Sample

Go ahead and open the attached sample. There is a new exclusion group, and for completeness I have also included the previous two examples.  When you open the sample, turn on field highlighting so that you can see when fields are mandatory (highlighted with a red border).  The sample shows a case where the user needs to select a minimum of one phone feature and a maximum of three.  Note these behaviors when you fill the form:

  • On opening the empty form, the fields are all highlighted red (mandatory) because none are selected — we are below the minimum selected.
  • When you select one entry, the mandatory setting is turned off. 
  • When you get to three selected entries, the remaining unselected entries are made read-only in order to prevent the user from selecting too many. 
  • If any entries are subsequently unselected, then the read-only setting is removed.

Code Changes

Initialization

There is now an initialization script on the exclusion subform to establish the minimum and maximum entries:

utility.setMinAndMax(this, 1 /* min */, 3 /* max */);

When users drags this object in from the object library, they need to tweak the initialization script to get the settings they want.  Note that if they set the maximum to one, then we use the toggling behavior when selecting entries (selecting one entry unselects another).  If they set a maximum greater than one we use the explicit select/unselect ui experience.

New functions

The script includes three new functions setMinAndMax(), setReadOnly() and setMandatory() plus new logic in makeExclusive() to set the mandatory and read-only states of exclusion group members.  From the behaviors described above and the code comments you can probably figure out how they’re used.

isSelected()

The logic inside isSelected() needed to be changed in order to support Reader 7.  Previously we relied on the selectedIndex property to determine if a checkbox was selected.  When selectedIndex is zero, the field is considered on/selected.  Grammar-wise, this is represented by this markup:

<field>
  <items>
    <text>yes</text>
    <text>no</text>
  </items>
</field>

selectedIndex refers to the position in the current chosen element in the <items> array. Since selectedIndex was not introduced until Reader 8, the alternate (Reader 7 compatible) strategy is to compare the value of the checkbox field to the first element under <items>.  If they are equal, the field is selected.

What Else?

I debated whether to expand on the logic behind isSelected().  For numeric fields, you might consider a value of zero to mean "not selected".  Also, there is probably more that could be done to support mandatory logic inside nested subforms.  For now I will leave these as an exercise for the user :-)

Summary

This is probably a good time to issue a caveat emptor.  These samples are intended to encourage some specific design patterns.  They are not supported objects for you to immediately use in your production forms.  You need to adapt them to your own needs and you need to test them in your own environments.

The main points I hope you take away from this exercise:

  • You can package your desired exclusion group experience into library objects without requiring users to add code to individual entries in the group
  • Tweak the provided sample to your own needs and include it in your object library
  • Centralize the script logic — do not have more than one copy in your form

Exclusion Groups v2.0

This is the second blog entry describing a DIY exclusion group. If you have not read part one, you should probably start there before trying to digest part two.

I wanted to build on the functionality of the exclusion group from the previous blog entry:

  1. Allow the form author to include nested subforms and radio button lists within the exclusion group
  2. Improve the handling of different field types
  3. Move the script logic outside the exclusion subform

The sample

The example is a form to choose a payment type. The payment type is an exclusion group with three kinds of content: a checkbox, radio button group and subform. The user chooses one of: cash (checkbox), cheque (radio button list specifying kind of cheque) or credit card (subform with all the credit card fields). When they choose a payment type, then the other payment types are cleared. Here is the sample.

Nested Subforms

Notice that the credit card fields are wrapped in a subform. The subform behaves as a single unit in the exclusion group. When any field inside the subform is selected, the subform is considered to be selected.

Field Types

The form in my previous post handled only check boxes and text/numeric fields. This sample includes handling for choice lists and radio button groups. The logic to check if a field is selected or not varies by field type.  This version of the exclusion group is more rigorous in its checking. Similarly, the logic to clear an entry varies according to the kind of object.

Separate the script

You will notice in the form that I moved the script into a script object called “utility”.  This is so that the script is not included (and duplicated) each time the form author includes another exclusion subform on their form.  It does have the downside that the form author needs to remember to add the utility script to the form.

To make this more foolproof, I have added a simple check at the beginning of the exclusion group calculate:

if (typeof(utility.makeExclusive) == “undefined”)
xfa.host.messageBox(
“You need to include the utility script object!”);

If utility.makeExclusive is found, the JavaScript typeof() function will return: “function” otherwise it returns “undefined”.
If your form author has forgotten the script (or put it in the wrong place) this simple check will notify them the first time they preview the form.

The Algorithm

Just as in the previous post, you do not really need to read past this point if you do not want gory details. Just take the sample, remove the contents from the exclusion subform and add the subform to your object library.  Do the same for the utility script. The whole point is that the logic is self contained, reusable and you can add content without writing any new script.

Hopefully reading the script code is self-explanatory, so I’ll just highlight a couple points here:

Get field type

In order to check the field type, the script needs to examine the content under the <field><ui> element. In the XFA grammar, this element is expressed as a “one of” relationship. The widget-type-child of <ui> must be one of: <barcode>, <button>, <checkButton>, <choiceList>, <dateTimeEdit>, <defaultUi>, <exObject>, <imageEdit>, <numericEdit>, <passwordEdit>, <signature> or <textEdit>. The convenient way to select this element is with the oneOfChild script property. So then the code to check if a field is a check button looks like:

if (vContainer.ui.oneOfChild.className == “checkButton”)

Code Additions

There are new functions: isSelected() and clearContainer(). Previously we assumed all members of the exclusion group were simple fields. Checking if a field was selected or clearing a field were one-liners. Now that we support subforms and more field types, these operations are more complex and we modularize them into functions.

That’s all for now.  Next up: min and max selections.