Copyright Notice

This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.

This text has appeared in an edited form in WebTechniques magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.

Please read all the information in the table of contents before using this article.
Download this listing!

Web Techniques Column 40 (Aug 1999)

[suggested title: Handling multi-page forms]

Handling forms with the all-singing all-dancing CGI.pm module is not a difficult task. Just toss up a web page with the right form elements, add a submit button, then wait for the reply, and call param to get the data.

But what if the form is too long to comfortably fit on one page? How can we allow the visitor to keep pressing next page until they're done entering all of the numerous items we've asked them to fill out? And worse yet, those darn users sometimes make mistakes, and want to back up!

Well, one easy technique is to use client-side storage, in the form of hidden fields. The main advantage is that we get this storage for free (think of it as extremely distributed memory). The downside is that people can (and will) hack the data, leading to potential security issues if you don't allow for that. Also, if there are many fields to fill out, it's gonna be potentially painful on a slow data link as the same values get passed repeatedly between the browser and the server.

But, to use hidden fields, you have to generate them. The mess of trying to determine which hidden fields to send out, and then setting up all those individual forms might seem a bit daunting, unless you let Perl help out.

Let's look at a typical multi-page form application: a ``how did we do?'' survey. The first page might gather general information about the visitor, and the second page might ask some questions about one particular area. In order for the first answers to be preserved when the second page is put up, you must include them as hidden fields in the second form. Then, when the visitor is submitting the form, you get not only the new answers, but also the values of the previous fields. If the visitor wants to go back, you generate hidden fields of the stuff in later pages already visited, until the visitor hits the final page, where you thank them for taking time, and save the data for later processing.

The other hard part about this stuff is all the common HTML amongst the pages. Well, we can simplify that as well, by providing a data-driven page layout. So, we'll be specifying only that one of the fields is a street address, and another is how they felt about a particular item, and let the code figure out how to include it into a form with a proper heading and layout.

My sample program that shows both techniques is presented here in [listing one, below]. I'm indebted to fellow columnist and CGI.pm creator Lincoln Stein, for presenting a tutorial example at a recent conference that inspired me to write this code.

Lines 1 through 5 begin nearly all the CGI scripts that I write, turning on taint mode (-T), warnings (-w), enabling compile-time restrictions for large programs (use strict) and pulling in all of the HTML-generation and form-access shortcuts from CGI.pm (beginning with use CGI).

Lines 7 through 68 define the configuration section. This is the portion of the program I would edit to make a different multi-page form.

Line 12 defines the DEFAULT_FIELD subroutine. This subroutine is called by the form generation for what we hope is the majority of the fields. This subroutine will be passed the name of the field as its first parameter. Here, I'm using the textfield shortcut (defined by CGI.pm) to generate a simple text box, with a width of 60.

Lines 14 through 39 define the successive pages for this form. Each element of @PAGES is an array reference, here given using the anonymous array constructor. Buttons for next and previous will advance and retract along this array, so it's important to get the page order correct. The first page will be issued when we first come to this script, and the last page will be shown when the next button is hit on the next-to-last page, and should tell the visitor that the form has been submitted.

Within each array reference, the first element must be the title of the page. This title will also be shown as an H1 heading within the page. After the title comes one of two things: either a coderef to generate the entire page (without any form elements) or a list of arrayrefs describing the various form elements.

Each form element in turn has two or three items in its list. The first is the form field name, the second is the descriptive text, and the third (if present) is a coderef to generate the form element. If the coderef is absent, then the default form type is used (defined earlier). The subroutine will be passed a single parameter of the form field name.

For this particular example, I'm showing series of pages to get information on a visit to a movie theatre. On line 17, we've got the opening page, which is generated from a subroutine intro_page, defined later. A next button on this page brings us to the first form-fillout page, defined in lines 18 through 24. Here we're asking for information about the visitor. All fields in this page will be generated using the default form field type.

Moving on, lines 25 through 30 define the next page, with one default field and one specially constructed field. The third element of the list that begins with Review is an anonymous subroutine that takes the first parameter (here, the word Review) and constructs a radio group using the CGI.pm-provided shortcut. We'll get six buttons (or the browser's equivalent) for this form element. If I had had a series of buttons with the same shape, I'd have created a named subroutine for this such as rating_field, and then used it multiple times with \&rating_field as the third element.

Lines 31 through 35 define the next page, with two default (text) fields again, and a textarea, again using an anonymous subroutine.

Lines 36 and 37 define the final two pages. There's no forward or back buttons on the final page (to keep the form from being resubmitted multiple times), so the next-to-last page advises the visitor to return back if any last minute changes must be made. The subroutines are defined later.

Lines 42 and 43 define the text that appears on the previous and next buttons. Because of the way the submit button works, the text must not collide with any form names defined above. So, I can change the text on these buttons as long as I pick names that are distinct from any of my chosen field names.

Line 44 defines the fieldname of an internal field to keep track of which page we're looking at. The only requirement is that this field be named distinct from any other field names I've used.

Next, we get to the subroutines to which I've taken references above. Since these are really single-use subroutines, I could have also inlined each one of them as an anonymous subroutine above, similar to how I did the odd-shaped fields.

Lines 46 through 51 define the initial page. This page has only a next button, and should let the visitor know what they're getting into. I'm generating two paragraphs here.

Lines 53 through 58 define my next-to-last page, where I tell the visitor that this is the last chance they'll have to back up to modify any previous entries.

Lines 60 through 66 define the final page. Here's where all the form elements are as final as they're going to get, because there won't be any back button from here. I'm generating two paragraphs, and then using the CGI.pm dump operation to show all the form values, as a demo. In real code, the form values would be accessed using param. Be sure to ignore the three internal-use form values though.

So, that's the end of the configuration section. Now we get to the real code.

Lines 70 through 72 fetch the three internal parameters, which may or may not be present. If absent, the created variables will get undef, tested later.

Lines 74 through 78 figure out what page we're on, and where to go next. If the $page variable is reasonable looking, we'll use it to mark the current page, and then go ahead or back depending on the presence of the $previous submit button. If anything is suspicious, we'll start back at page 0.

Line 79 sets the form element for the current page number to the internal idea of the page number. This makes it easy to generate a hidden field with the current page number later.

Line 81 grabs the arrayref for the particular page of interest from the @PAGES array defined above. The first element of this resulting array is the page title, shifted off in line 82.

Lines 83 and 84 generate the top of the page, including the beginning of a form. This form will have as an action the very same script that is already running, with a method of POST.

If the first element of the remaining list is a coderef, that's the subroutine that will display the entire page, so we test for that in line 86 and call it in line 87, passing no parameters. Otherwise, it's a form page, and it's time to generate the form.

To make everything line up nice, I've put the form elements into a table, with a narrow border and enough cellpadding to keep the text from bumping into the ruling. The rows of the table come from the map operator, wrapping a Tr shortcut around a th and a td. The contents of the th cell come from the human-readable string given as the second list element, while the contents of the td cell are our form element. Here, we'll take the third list element, if present, or the default field subroutine, and call it as a coderef, passing the first list element. And out pops our nicely formed table!

Lines 97 to 100 generate the appropriate next or previous buttons, using the rules specified earlier. The fieldname for the button is the same as the text on the button.

Now, to make the form values sticky as we progress backwards and forwards through the multiple pages, we have to dump everything we know so far using hidden fields. This is handled in lines 102 through 112. First, line 103 dumps the page number, which we look at in subsequent visits to know how far along we've gotten.

Then, lines 104 through 112 walk through the descriptions of all the remaining pages. What we're looking for are form elements that are defined that had been given values other than on the page we're on. Line 105 avoids the current page. Lines 106 and 107 extract the info (similar to the code earlier), and line 108 avoids subroutine-defined pages (without form elements). Finally, if we've made it to the loop in lines 109 through 111, it's time to dump what we know. Each form field's name is extracted from the array in @info, and the corresponding hidden field is included in the output.

Finally, lines 113 and 114 finish up the page.

That's it. A short program to handle a multiple page form, missing only the final processing (although I showed you where it would go). So, don't fear the multi-page form any longer. No need to stay hidden when they ask you to do that. Just use a hidden field. Until next time, enjoy!

Listings

        =1=     #!/usr/local/bin/perl -Tw
        =2=     use strict;
        =3=     $| = 1;
        =4=     
        =5=     use CGI ":all";
        =6=     
        =7=     ### configuration
        =8=     
        =9=     ## all-capital names are used in code, so don't change them
        =10=    
        =11=    ## type of default fields unless overridden with $_[0] is fieldname
        =12=    sub DEFAULT_FIELD { textfield(shift, "", 60) }
        =13=    
        =14=    my @PAGES =
        =15=      (
        =16=       ## first and second pages have no back buttons
        =17=       ["Introduction", \&intro_page],
        =18=       ["Tell us about yourself",
        =19=        [Name => "Name"],
        =20=        [Address => "Address"],
        =21=        [City => "City"],
        =22=        [State => "State"],
        =23=        [Zipcode => "Zip Code"],
        =24=       ],
        =25=       ["Tell us about your experience",
        =26=        [Movie => "Movie you saw"],
        =27=        [Review => "What you thought of it", sub {
        =28=           radio_group(shift, ['No opinion', qw(Excellent Good Fair Poor Bombed)]);
        =29=         }],
        =30=       ],
        =31=       ["Tell us anything else",
        =32=        [Theatre => "Theatre conditions"],
        =33=        [Snackbar => "Snack bar"],
        =34=        [General => "Any other comments?", sub { textarea(shift) }],
        =35=       ],
        =36=       ["Thank you!", \&thank_you_page],
        =37=       ["Goodbye!", \&submit_page],
        =38=       ## last page has no back or forward buttons
        =39=      );
        =40=    
        =41=    ## Internal use: these must not collide with fieldnames above
        =42=    my $PREVIOUS = "Previous Page";
        =43=    my $NEXT = "Next Page";
        =44=    my $PAGE = "__page__";
        =45=    
        =46=    sub intro_page {
        =47=      print
        =48=        p(["Thank you for visiting ScratchySound Theatres!",
        =49=           "Please take a moment to tell us about your experience.",
        =50=           ]);
        =51=    }
        =52=    
        =53=    sub thank_you_page {
        =54=      print
        =55=        p(["Thank you for taking the time to fill out our survey!",
        =56=           "Go back now to change any entries you need, or forward to send to us!",
        =57=          ]);
        =58=    }
        =59=    
        =60=    sub submit_page {
        =61=      ## code to save params would go here
        =62=      ## be sure to ignore param($PAGE), param($NEXT), param($PREVIOUS)
        =63=      print
        =64=        p(["Your entry has been submitted to us!", "Thank you!"]);
        =65=      print $CGI::Q->dump;          # debugging
        =66=    }
        =67=    
        =68=    ### end
        =69=    
        =70=    my $previous = param($PREVIOUS);
        =71=    my $next = param($NEXT);
        =72=    my $page = param($PAGE);
        =73=    
        =74=    if (defined $page and $page =~ /^\d+$/ and $page >= 0 and $page <= $#PAGES) {
        =75=      $page += defined($previous) ? -1 : +1; # default to forward
        =76=    } else {
        =77=      $page = 0;
        =78=    }
        =79=    param($PAGE, $page);            # set it for hidden
        =80=    
        =81=    my @info = @{$PAGES[$page]};
        =82=    my $title = shift @info;
        =83=    
        =84=    print header, start_html($title), h1($title);
        =85=    print start_form;
        =86=    if (ref $info[0] eq "CODE") {
        =87=      $info[0]->();
        =88=    } else {
        =89=      print table({ Border => 1, Cellpadding => 5 },
        =90=                  map { Tr(
        =91=                           th($_->[1]),
        =92=                           td(($_->[2] || \&DEFAULT_FIELD)->($_->[0]))
        =93=                          )} @info
        =94=                 );
        =95=    }
        =96=    
        =97=    ## no backing up from first or second or last page:
        =98=    print submit($PREVIOUS) if $page > 1 and $page < $#PAGES;
        =99=    ## no going forward from last page:
        =100=   print submit($NEXT) if $page < $#PAGES;
        =101=   
        =102=   ## generate hidden fields
        =103=   print hidden($PAGE);
        =104=   for my $other (0..$#PAGES) {
        =105=     next if $other == $page;      # don't dump hiddens for current
        =106=     my @info = @{$PAGES[$other]};
        =107=     shift @info;                  # toss title
        =108=     next if ref($info[0]) eq "CODE"; # code page
        =109=     for (map {$_->[0]} @info) {
        =110=       print hidden($_) if defined param($_);
        =111=     }
        =112=   }
        =113=   print end_form;
        =114=   print end_html;

Randal L. Schwartz is a renowned expert on the Perl programming language (the lifeblood of the Internet), having contributed to a dozen top-selling books on the subject, and over 200 magazine articles. Schwartz runs a Perl training and consulting company (Stonehenge Consulting Services, Inc of Portland, Oregon), and is a highly sought-after speaker for his masterful stage combination of technical skill, comedic timing, and crowd rapport. And he's a pretty good Karaoke singer, winning contests regularly.

Schwartz can be reached for comment at merlyn@stonehenge.com or +1 503 777-0095, and welcomes questions on Perl and other related topics.