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);
}