Friday, June 1, 2012

Declaratively tie the visibility of form fields to a checkbox value

Fields Contingently Displayed Based on a Checkbox 

In theory one of the great benefits of moving forms to the computer from paper is the ability to hide away sections of the form which are irrelevant to the user. We've all had the experience filling out tax forms where large sections of fields were dedicated to a case which just didn't apply to us, leading to a disagreeable proliferation of superfluous fields which nevertheless require inspection to verify they truly are irrelevant. If we can hide away those superfluous fields in the computer versions of our forms then we will have nicely simplified -- and reduced -- the work. Obviously there is ample support for making divs invisible, but the control of the underlying logic can make pages' internal logic complicated. When I was recently putting together a long form which needed this treatment, I wasn't looking forward to larding the page with ad hoc JavaScript to suppress superfluous fields depending on user settings. What I wanted was a declarative way to get this logic. Something like the following:
    <span id='checkbox_contingent_additional_person' visible=no>
        <table border=1 width=100% >
            <tr>
                <td>First Name</td>
                <td><input type=text id=some_relative_First_Name__0</td>
            </tr>
            <tr>
                <td>Last Name</td>
                <td><input type=text id=some_relative_Last_Name__0</td>
            </tr>
        </table>
    </span>  

I wanted to use spans for the outer layers so that if there were some browser compatibility problem with my added functions, there would be a graceful degradation to simply displaying the entire form. But what I wanted was for special logic to come in to play for spans with IDs beginning with the substring checkbox_contingent. I wanted also to support an attribute called visible which would determine whether the fields enclosed by the span would be initially displayed or suppressed.

I was able to implement this using jquery and the following code:


function checkbox_contingency_init()
{
    $("span[id^=checkbox_contingent_]").each(function(index, outer_span_dom)
    {
        var outer_span = $("#" + outer_span_dom.id)
        var prefix = outer_span_dom.id.replace(/checkbox_contingent_/, "")
        var checkbox_id = "conditional_checkbox_" + prefix
        var span_name = checkbox_id.replace(/checkbox/, "span") // + "_" + this.value
        var prompt = prefix.replace(/_/g, " ")
        var checkbox_html = prompt + "? <input id='" + checkbox_id + "' name='" + checkbox_id + "' type='checkbox' /><br>"
        //debugger
        var user_html = outer_span.html()
        var span_html = "<span id=" + span_name + ">" + user_html + "</span>"
        outer_span.html(checkbox_html + span_html)
        var variable_span = $("#" + span_name)
        var visible = outer_span.attr("visible")
        if (visible && visible=="yes")
        {
            variable_span.show()
            var checkbox = $("#" + checkbox_id)
            checkbox.attr("checked", true)
        }
        else
        {
            variable_span.hide()
        }

        $("#" + checkbox_id).change(function()
        {
            if (checkbox_id != this.id) throw "oops mismatch: " + checkbox_id + " != " + this.id

            //alert('looking for span '+ span_name)
            var span = $('#' + span_name)
            if (!span)
            {
                throw ("no span matching " + span_name)
            }
            var val = this.checked
            if (val)
            {
                span.show()
            }
            else
            {
                span.hide()
            }
        })
    })
}


$(document).ready(
function()
{
    checkbox_contingency_init()
})



Using jquery I am able to find all the spans with IDs beginning with the substring checkbox_contingent. For each of these spans I rewrite the HTML to include a checkbox which controls whether the form fields are displayed.

Expandable Arrays 

The technique described above worked nicely for my purpose. As I continued, I realized I had repeated work in a variation on my use case, where in some cases it would be appropriate to show the user fields for some array of entries of indeterminate length. In this case what I wanted was to repeatedly show a checkbox which would allow the user to indicate that he wanted to enter data for yet another entry. Each time he made such an indication by checking the checkbox, a new cell of field forms would be displayed (along with an additional checkbox to allow further extending the entries). Here is the HTML syntax I wanted to use:


        <span id='expandable_array_more_children' visible=yes max_length=3>
   <table border=1 width=100% >
       <tr>
           <td>First Name</td>
           <td><input type=text id=more_children_First_Name__0</td>
       </tr>
       <tr>
           <td>Last Name</td>
           <td><input type=text id=more_children_Last_Name__0</td>
       </tr>
   </table>
</span>
       


In this case the span would contain the fields for a single entry. My jquery code would replace this HTML with a checkbox to enable the showing of the field. Each time the user checks the checkbox, another entry in the array is displayed, along with an additional checkbox to indicate that further entries are appropriate. I also put in support for a max_length attribute to limit the total number of entries. Here is the code to accomplish this: 

function expandable_array_init1(html_to_show_first, span, prefix, x)
{
    if (!html_to_show_first)
    {
        html_to_show_first = ""
    }
    if (x==null)
    {
        span.html(html_to_show_first)
    }
    else
    {
        var checkbox_id = "conditional_checkbox_" + prefix + "_" + x
        var span_id = checkbox_id.replace(/checkbox/, "span")
        var prompt = prefix.replace(/_/g, " ")
        if (!x) // first time through?
        {
            prompt = prompt.replace(/^more /, "")
        }
        span.html(html_to_show_first + "<br>" + prompt + "? <input id='" + checkbox_id + "' type='checkbox' value='" + x + "' /><br><span id=" + span_id + "/>")
        
        
        var checkbox = $("#" + checkbox_id)
        checkbox.change(function()
        {
            assert(checkbox_id == this.id) 
            var span = $('#' + span_id)
            assert(span)
            var val = this.checked
            if (val)
            {
                var x = parseInt(this.id.replace(/.*_/, ""))
                
                var pattern    = document.expandable_array_patterns[prefix]
                var max_length = document.expandable_array_max_length[prefix]
                assert(pattern)
                var filled_pattern = pattern.replace(/__0/, "__" + x)
                
                var next_checkbox_x = ((max_length==null) || (x < max_length-1)) ? (x+1) : null
                expandable_array_init1(filled_pattern, span, prefix, next_checkbox_x)
                
                span.show()
            }
            else
            {
                span.hide()
            }
        })
    } 
    return checkbox
}

function expandable_array_init()
{
    $("span[id^=expandable_array_]").each(function(index, outer_span_dom)
    {
        var outer_span = $("#" + outer_span_dom.id)
        assert(outer_span)
        var prefix = outer_span_dom.id.replace(/expandable_array_/, "")
        if (!document.expandable_array_patterns)
        {
            document.expandable_array_patterns = new Object()
            document.expandable_array_max_length = new Object()
        } 
        document.expandable_array_patterns[prefix] = outer_span.html()
        var max_length = outer_span.attr("max_length")
        if (max_length)
        {
            document.expandable_array_max_length[prefix] = parseInt(max_length)
        }
        outer_span.html("")
    
        var checkbox = expandable_array_init1(null, outer_span, prefix, 0)
        var visible = outer_span.attr("visible")
        if (visible && visible=="yes")
        {
            checkbox.attr("checked", true)
            checkbox.change()
        }
    })
}


$(document).ready(
function()
{
    expandable_array_init()
})


In this case I use jquery to search for spans whose IDs begin with expandable_array. For each of these spans I replace the HTML with a checkbox and an additional embedded span which when visible shows the fields responding to a single entry. I use the initial HTML within the span as a pattern from which I can create new fields for additional entries. It is expected that each field within the HTML has a naming convention ending with the index of the entry it refers to; the initial field IDs all and with __0; as new entries are generated, I use the exact same HTML except with the index incremented, resulting in similar field names ending with __1, __2, __3, etc.

No comments:

Post a Comment