Illegal Form Field Demonstration

Introduction

This web page is a demonstration of a technique for dynamically activating and deactivating form fields depending on the status of other fields.

While there are methods of using dynamic HTML tricks to make a field appear and disappear based on the content of another form element, they aren't reliable or cross-platform because they use positioning. Positioning is not consistently implemented or implemented at all in various browsers. Examples of these techinques can be found at javascript.faqts and The DHTML Learning Curve.

My technique is to use javascript to prevent the user from focussing on fields that should not be filled in depending on the status of other elements. In the case that this behaviour is confusing to a user who psersistently attemptemts to focus on an element that is not supposed to be focussed on, an error message is displayed that explains this.

Demo

The form below is a demonstration of my techniques showing several different methods of doing the same thing.

To see it do it's thing, try to click in the Project #1 (Due) Date field. You'll notice that the focus automatically moves to another field. This is because the status is Project Requested by, which, logically needs a person's name associated with it, but not a date. Consequently, the Authorization field is a legal field. You can easily change the focus to the Authorization field because it is a legal field. Change the Poject status to Project Approval Expected by and you'll notice that the authorization field is automatically erased and that the focus moves to the (Due) Date field, where you can enter the date that the approval is expected by. However, it is no longer possible to focus on the Authorization field. Give it a try anyway. The focus will skip, but try again a few more times and you'll get an error message explaining why you can't focus on that field. Selecting a Project Status of Project In Progress locks out BOTH the (Due) Date and the Authorization fields. All of the other rows do the same sort of thing, but use slightly different techniques.

Project Name Project Status (Due) Date Authorization Technique
Project #1 dontUpdateElementWhen, legalFieldIf
Project #2 updateElementWhen, illegalFieldIf
Project #3 dontUpdateElementWhenString, legalFieldIfString
Project #4 updateElementWhenString, illegalFieldIfString

How it works

You can see the source code by looking at the source for this web page or by looking at the source presented below.

There are two main types of function that are used by this web page. The first are the functions that determine if a field is legal or not. The second set are functions that determine whether or not to update other fields when changes are made. In addition, there are some support functions that are used to keep track of which direction the focus is moving.

There is a generic routine for each of the two main function types and wrappers so that the funtion can be used in different ways. One set of possibilities allows matching lists of values or strings. Another set makes the function active on the condition matching and the other makes the function inactive on the condition matching. The different cases are shown below:

Functions that determine field legality
Matches on selected value Matches on selected string
Active on match legalFieldIf legalFieldIfString
Inactive on match illegalFieldIf illegalFieldIfString

Functions that update other elements
Matches on selected value Matches on selected string
Active on match updateElementWhen updateElementWhenString
Inactive on match dontUpdateElementWhen dontUpdateElementWhenString

Source Code

The following should be put in the HEAD part of the web page (ie: between the <HEAD> and the </HEAD> tags).

    <SCRIPT LANGUAGE="Javascript1.2">
      <!-- Hide script from old browsers

      /** SCRIPT: Dependent Text Field Script
        * AUTHOR: Mark Crocker
        * EMAIL: mcrocker@markcrocker.com
        * Web: http://www.markcrocker.com/~mcrocker
        * Version: 0.91
        */

      var lastElementIndex = "";
      var lastLastElementIndex = "";
      var lastSkipIndex = "";
      var lastLastSkipIndex = "";
      var direction = 1;

      /* Skip the element field if the value of the
       * status selection is in the skip values array.
       */
      function illegalFieldIf(element,statusElement,statusSkipValues) {
        var elmntIndx = elementIndex(element);                              // Find element index in form array
        trackElement(elmntIndx);                                            // keep track of where we have been
        if (inTheList(statusElement,statusSkipValues) == 1) {               // skip if the current status selection IS in the list
          skipAction(elmntIndx);                                            // skip the element
         warnPersistentUsers(elmntIndx,statusElement);                       // Produce a warning if user persistently tries to select this element
        }
      }


      /* Skip the element field if the value of the
       * status selection is NOT in the skip values array.
       */
      function legalFieldIf(element,statusElement,statusDontSkipValues) {
        var elmntIndx = elementIndex(element);                              // Find element index in form array
        trackElement(elmntIndx);                                            // keep track of where we have been
        if (inTheList(statusElement,statusDontSkipValues) == 0) {           // skip if the current status selection is NOT in the list
          skipAction(elmntIndx);                                            // skip the element
         warnPersistentUsers(elmntIndx,statusElement);                       // Produce a warning if user persistently tries to select this element
        }
      }


      /* Skip the element field if any string in the
       * skip strings array CAN be found in the selected status text.
       */
      function illegalFieldIfString(element,statusElement,statusSkipStrings) {
        var elmntIndx = elementIndex(element);                              // Find element index in form array
        trackElement(elmntIndx);                                            // keep track of where we have been
        if (inTheStringList(statusElement,statusSkipStrings) == 1) {        // skip if the current status selection IS in the list
          skipAction(elmntIndx);                                            // skip the element
         warnPersistentUsers(elmntIndx,statusElement);                      // Produce a warning if user persistently tries to select this element
        }
      }


      /* Skip the element field if any string in the
       * skip strings array can NOT be found in the selected
       * status text.
       */
      function legalFieldIfString(element,statusElement,statusDontSkipStrings) {
        var elmntIndx = elementIndex(element);                              // Find element index in form array
        trackElement(elmntIndx);                                            // keep track of where we have been
        if (inTheStringList(statusElement,statusDontSkipStrings) == 0) {    // skip if the current status selection is NOT in the list
          skipAction(elmntIndx);                                            // skip the element
         warnPersistentUsers(elmntIndx,statusElement);                      // Produce a warning if user persistently tries to select this element
        }
      }


      /* Updates (clears) element field if the value of the
       * status selection IS in the update values array
       */
       function updateElementWhen(statusElement,dependentElement,statusUpdateValues) {
        var elmntIndx = elementIndex(dependentElement);                     // Find dependentElement index in form array
        if (inTheList(statusElement,statusUpdateValues) == 1) {             // if the value is NOT in the list, erase the element
          document.FirstForm.elements[elmntIndx].value = "";                // erease the field if it is to be updated
        }
       }


      /* Updates (clears) element field if the value of the
       * status selection is NOT in the update values array
       */
       function dontUpdateElementWhen(statusElement,dependentElement,statusUpdateValues) {
        var elmntIndx = elementIndex(dependentElement);                     // Find dependentElement index in form array
        if (inTheList(statusElement,statusUpdateValues) == 0) {             // if the value IS in the list, erase the element
          document.FirstForm.elements[elmntIndx].value = "";                // erease the field if it is to be updated
        }
       }


      /* Updates (clears) element field if any string in
       * the update string array CAN be found in the selected
       * status text
       */
       function updateElementWhenString(statusElement,dependentElement,statusUpdateStrings) {
        var elmntIndx = elementIndex(dependentElement);                     // Find dependentElement index in form array
        if (inTheStringList(statusElement,statusUpdateStrings) == 1) {      // if the value is NOT in the list, erase the element
          document.FirstForm.elements[elmntIndx].value = "";                // erease the field if it is to be updated
        }
       }


      /* Updates (clears) element field if any string in
       * the update string array CAN be found in the selected
       * status text
       */
       function dontUpdateElementWhenString(statusElement,dependentElement,statusUpdateStrings) {
        var elmntIndx = elementIndex(dependentElement);                     // Find dependentElement index in form array
        if (inTheStringList(statusElement,statusUpdateStrings) == 0) {      // if the value IS in the list, erase the element
          document.FirstForm.elements[elmntIndx].value = "";                // erease the field if it is to be updated
        }
       }


      /* Figures out if the selected status value is in the
       * list of status values that require some sort of action
       */
      function inTheList(statusElement,statusActionValues) {
        var statusElementIndex = elementIndex(statusElement);               // Find index of status field.
                                                                            // Selected Value from status field
        var statusValue = document.FirstForm.elements[statusElementIndex].options[document.FirstForm.elements[statusElementIndex].selectedIndex].value;
        var action = 0;                                                     // default to no action

        for (var i=0; i < statusActionValues.length; i++) {                 // go through list of values that trigger some action
          if (statusValue == statusActionValues[i]) {                       // if the value is in the list,
           action=1;                                                        // do not skip if the status is in the list
           break;                                                         // no point in checking more, so quit now
         }
        }
        return action;
      }



      /* Figures out if the selected status text contains any of the
       * strings list of status strings that require some sort of action
       */
      function inTheStringList(statusElement,statusActionStrings) {
        var statusElementIndex = elementIndex(statusElement);               // Find index of status field.
                                                                            // Selected text from status field
        var statusText = document.FirstForm.elements[statusElementIndex].options[document.FirstForm.elements[statusElementIndex].selectedIndex].text;
        var action = 0;                                                     // default to no action

        for (var i=0; i < statusActionStrings.length; i++) {                // go through list of values that trigger some action
          for(var j=0; j < statusText.length-statusActionStrings[i].length+1; j++) {    // go through string to find matching substring
           if (statusText.substring(j,j+statusActionStrings[i].length) == statusActionStrings[i]) {
            action=1;                                                       // do not skip if the status is in the list
             break;                                                          // no point in checking more, so quit now
           }
        }
        }
        return action;
      }


      /* Action taken to skip an element
       */
      function skipAction(elmntIndx) {
        document.FirstForm.elements[elmntIndx].value = "";                  // erease the field if it is to be skipped
        document.FirstForm.elements[elmntIndx+direction].focus();           // move focus to the next element.
      }


      /* Warn user if they try to select an illegal element several
       * times in a row.
       *
       * Bug: This routine has a problem whenever two adjacent fields
       *      use it because it relies on keeping track of the last
       *      few skips and checking to see if they're the same.  When
       *      there are two in a row, this does not work.
       *
       */
      function warnPersistentUsers(elmntIndx,statusElement) {
        if ((elmntIndx == lastSkipIndex) && (lastSkipIndex == lastLastSkipIndex)) {
          alert("Attempt to move to field " +
              document.FirstForm.elements[elmntIndx].name +
                "\nis not allowed while field " +
                statusElement +
                "\nhas the value \"" +
              document.FirstForm.elements[statusElementIndex = elementIndex(statusElement)].options[document.FirstForm.elements[statusElementIndex].selectedIndex].text +
              "\".\nAutomatically skipping to field " +
              document.FirstForm.elements[elmntIndx+direction].name);
        }
        lastLastSkipIndex = lastSkipIndex;                                  // Keep track of where we've skipped before
        lastSkipIndex = elmntIndx;
      }



      /* Figures out what the form element index is from the
       * element name.
       */
      function elementIndex(element) {
        for(var index = 0; index < document.FirstForm.elements.length; index++) { // find element index in form array
          if (document.FirstForm.elements[index].name == element) {
            break;
          }
        }
        return index;
      }


      /* Keep track of where we've been and update
       * direction accordingly
       * Warning: all values changed by side-effects.
       */
      function trackElement(elementIndex) {
        if (lastElementIndex < elementIndex) {                              // set the direction depending on where you came from
           direction = 1;
        } else if (lastElementIndex > elementIndex) {
          direction = -1;
        }

          lastLastElementIndex = lastElementIndex;                          // Keep track of where we've been for error message and direction info
          lastElementIndex = elementIndex;
      }


      /* Todo:
       * 1. Fix warnPersistentUsers to be able to deal with multiple skip fields in a row.
       * 2. Get rid of hard-coded form reflection (ie: document.FirstForm...).
       * 3. Add documentation on how to use this to the web page.
       * 4. Come up with better funciton names!
       */

        // End hiding from old browsers -->
    </SCRIPT>

Usage Example

The following shows an example of using these tools.

The following should be put in the BODY part of the web page before the form elements that use the data (ie: between the <BODY> and the <FORM> tags).

    <SCRIPT LANGUAGE="Javascript1.2">
      <!-- Hide script from old browsers

      /* One way to specify dependencies is with array variables.
       * Predefining them like this saves having to do it within each
       * function call.
       */

      /* When the selected TEXT includes one of these these strings we
       * have a match
       */

      var dateStrings = new Array(2);
      dateStrings[0] = "Due";
      dateStrings[1] = "Expected";

      var authStrings = new Array(2);
      authStrings[0] = "Approved";
      authStrings[1] = "Requested";


      /* When the selected VALUE is one of the following, we have a
       * match.
       */

      var dateValues = new Array(2);
      dateValues[0] = "2"; // Project Approval Expected by
      dateValues[1] = "5"; // Project Completion Due


      var authValues = new Array(2);
      authValues[0] = "1"; // Project Requested by
      authValues[1] = "3"; // Project Approved by


      /* Same concept as above, but a briefer format and these are
       * used to indicate cases that are NOT a match.
       */

      skipDateStrings = new Array('Requested', 'Approved', 'Progress', 'Complete', 'Cancelled', '');
      skipAuthStrings = new Array('Expected', 'Progress', 'Due', 'Complete', 'Cancelled', '');

      // End hiding from old browsers -->
    </SCRIPT>


Valid HTML 4.0!