Recipe 19.11 Writing a Multiscreen CGI Script

19.11.1 Problem

You want a single CGI script that can return several different pages to the browser. For instance, you want a single script for administering a database of products. The script will be called to display the form to add a product, to process the add-product form, to display a list of products to delete, to process the delete-product form, to display a list of product to edit, to display a form of the product's attributes for the user to change, and to process the edit-product form. You can use these multiscreen CGI scripts to form an elementary shopping-cart application.

19.11.2 Solution

Use a hidden field to encode the current screen.

19.11.3 Discussion

It is easy to generate sticky hidden fields with the CGI module. The hidden function returns HTML for a hidden widget and uses the widget's current value if you pass hidden only the widget name:

use CGI qw(:standard);
print hidden("bacon");

To determine which page ("display product list", "display all items in shopping cart", "confirm order") to display, use another hidden field. We'll call this one .State so it won't conflict with any field we might have called State (for instance, in credit card billing information). To let the user move from page to page, use submit buttons that set .State to the name of the page to go to. For instance, to make a button to take the user to the "Checkout" page, use:

print submit(-NAME => ".State", -VALUE => "Checkout");

We wrap this in a function to make it easier to type:

sub to_page { return submit( -NAME => ".State", -VALUE => shift ) }

To decide what code to display, check the .State parameter:

$page = param(".State") || "Default";

Put the code to generate each page in separate subroutines. You could decide which subroutine to call with a long if ... elsif ... elsif:

if ($page eq "Default") {
    front_page( );
} elsif ($page eq "Checkout") {
    checkout( );
} else {
    no_such_page( );         # when we get a .State that doesn't exist

This is tedious and clumsy. Instead use a hash that maps a page name to a subroutine. This is another strategy for implementing a C-style switch statement in Perl:

%States = (
    'Default'     => \&front_page,
    'Shirt'       => \&shirt,
    'Sweater'     => \&sweater,
    'Checkout'    => \&checkout,
    'Card'        => \&credit_card,
    'Order'       => \&order,
    'Cancel'      => \&front_page,

if ($States{$page}) {
    $States{$page}->( );   # call the correct subroutine 
} else {
    no_such_page( );

Each page will have some persistent widgets. For instance, the page that lets the user order t-shirts will want the number of t-shirts to persist even when the user continues and orders shoes as well. We do this by calling the page-generating subroutines with a parameter that lets them know whether they're the active page. If they're not the active page, they should only send back hidden fields for any persistent data:

while (($state, $sub) = each %States) {
    $sub->( $page eq $state );

The eq comparison returns true if the page is the current page, false otherwise. The page-generating subroutine then looks like this:

sub t_shirt {
    my $active = shift;

    unless ($active) {
        print hidden("size"), hidden("color");

    print p("You want to buy a t-shirt?");
    print p("Size: ", popup_menu('size', [ qw(XL L M S XS) ]));
    print p("Color:", popup_menu('color', [ qw(Black White) ]));

    print p( to_page("Shoes"), to_page("Checkout") );

Because the subroutines all generate HTML, we have to print the HTTP header and start the HTML document and form before we call the subroutines. This lets us print a standard header and footer for all pages. Here, we assume we have subroutines standard_header and standard_footer for printing headers and footers:

print header("Program Title"), start_html( );
print standard_header( ), begin_form( );
while (($state, $sub) = each %States) {
    $sub->( $page eq $state );
print standard_footer( ), end_form( ), end_html( );

Don't make the mistake of encoding prices in the forms. Calculate prices based on the values of the hidden widgets, and sanity-check the information where you can. For example, compare against known products to make sure they're not trying to order a burgundy XXXXXXL t-shirt. You can identify items by any string, whether it's a text string like "sweater_xl_plain" that's a key to a hash of prices, or whether you want to use a product "SKU" number that you look up in an external database.

Using hidden data is more robust than using cookies, because you can't rely on the browser supporting or accepting cookies. A full explanation is in Recipe 19.9. On the other hand, using hidden data means that every link the user follows must be a form submit button instead of a normal hyperlink.

We show a simple shopping cart application as the program chemiserie at the end of this chapter.

19.11.4 See Also

The documentation for the standard CGI module.