[Catalyst] An initial version of a form rendering and validation module

Andrew Ford A.Ford at ford-mason.co.uk
Tue Nov 22 13:18:12 CET 2005


I have been working away on a module called CGI::FormManager for a 
couple of months now (I registered it with CPAN a month or so ago and 
gave a talk on it at the Birminham perl mongers meeting at the end of 
October).  The module is intended to render forms from definitions of 
the forms and validate submitted data against those definitions.  It is 
inspired in part by CGI:FormBuilder, but delegates validation to 
Data::FormValidator.  I intend to use it with Catalyst and Template 
Toolkit, but of course its application is wider than that.

Given that the module works with Catalyst I am making a first 
announcement of version 0.04 here.  The source code is available from:

    http://andrew-ford.com/perl/cgi-formmanager/

Bear in mind that this is alpha-stage code -- I may change any aspect of 
it at the moment, although I do have a client using an even earlier 
version on a production web site.

I've attached an introduction to the module below.

I would welcome comments on the design and on any other aspect of it.

Andrew



NAME
    CGI::FormManager::Manual::Intro - introduction to the Form Manager

INTRODUCTION
    Coding for CGI forms is tedious. Often an application that accepts user
    data will display an initial version of a form, check the user's data
    then display another version of the form indicating errors in the data;
    then when the data passes all validation checks it may display a
    confirmation page containing the user's data, allowing the user to
    confirm, cancel or go back and edit the data.

    That is a lot of code - a lot of very tedious code. What I would prefer
    to have is a definition of the structure of the form and have all the
    code generated for me. This is what "CGI::FormManager" attempts to do --
    it manages forms, validating and rendering them. It is inspired by
    "Data::FormValidator" and "CGI::FormBuilder".

    The module renders form with initial data, with data submitted by the
    user, and will also render a confirmation form. It uses an internal
    default templates render form using tables (this may change to CSS at
    some stage). By default forms are laid out with the labels in one column
    and the fields in the next column, but there are also facilities to deal
    with repeating groups of elements (such as a checkout form), and I am
    thinking of providing the facility to specify groups of fields (for
    example for a billing address and a shipping address laid out side by
    side).

    Validation is delegated to "Data::FormValidator", but the form
    validation profile specified is augmented with information derived from
    the definition of the fields making up the form.

    NOTE: this module is still experimental and in a state of flux. I am
    still playing with the API and the manner of declaring forms, and am
    trying to get it all to "feel right".

AN EXAMPLE
    Our example application is a fairly typical user registration
    application with an initial data entry page and a data confirmation
    page. The application uses Template Toolkit for rendering and Catalyst
    as the application framework.

  The HTML Templates
    The template for the initial data entry page would look like:

        <h1>Sample App</h1>

        <p>Some instructions here.</p>
        <p>Please fill in this form:</p>

        [% form %]

    Our Template Toolkit setup will wrap the output of the template in
    "<html>" and "<body>" elements, and insert a "<head>" element and all
    the other paraphernalia to give a complete HTML document.

    The "form" variable is set up in the application code, which we will
    come to later. It is a "CGI::FormManager::Form" object and as such
    renders itself as HTML in a string context.

    If there are errors in the data submitted then we go to the corrections
    page:

        <h1>Sample App</h1>

        <p>There were some problems with the data you entered</p>
        <p>[% message %]</p>

        [% form %]

    Of course this fragment and the previous one are so similar that one
    might want to roll them together. [EXPAND]

    When we are happy with the data we display the confirmation form:

        <h1>Sample App</h1>

        <p>Thank you for entering your details.  Please check the data
        you entered and click confirm, edit or cancel,</p>

        [% form.render_confirmation %]

  The Form Definition
    The form is defined in a hash passed to the "CGI::FormManager" package.
    The hash contains three elements: "options", "elements" and
    "validation".

    This is what our form definition might look like:

      my $form_defn = {
          options    => { method => 'POST',
                          action => $url,
                        },
          elements   => [ '<h2>Your login credentials</h2>',
                          cfm_text_field( name       => 'email',
                                          label      => 'Email address',
                                          size       => 40,
                                          maxlength  => 50,
                                          constraint => email()
                                          REQUIRED ),
                          cfm_password_field( name  => 'password',
                                              REQUIRED),
                          cfm_password_field( name  => 'password2',
                                              label => 'Repeat your 
password',
                                              REQUIRED),

                          '<h2>Personal Details</h2>',
                          cfm_text_field( name       => 'first_name',
                                          REQUIRED ),
                          ...

                        ],
          validation => { required => [ qw(email password) ],
                          constraint_methods => {
                              email => email(),
                          },
                        },
      };

    The "options" element is fairly self-explanatory, specifying the URL for
    the form and that the form should be submitted with a POST request.

    The "elements" element lists the "elements" that make up the form.
    "CGI::FormManager" exports a set of subroutines for creating form
    elements of different types; any plain text strings are regarded as a
    literal text to be included in the form. Fields are named and may have a
    label, which will be included in front of the field element in the HTML
    document. If no label is specified then a label is created from the
    field name by upper-casing the first letter and replacing underscores
    with spaces. Constraints may be specified in the field constructors or
    may be specified in the "validation" element.

    The "validation" element specifies how the form should be validated. It
    is augmented with information inferred from the field elements and
    passed to "Data::FormValidator" when the form is validated.

The Application Code
    The sample application is a Catalyst application, using a Template
    Toolkit view component. See the Catalyst documentation for more
    information.

    The main application package instantiates a "CGI::FormManager" object to
    manage the forms (which are defined in the controller packages), storing
    it in the application's configuration and then sets up the Catalyst
    application.

      package MyRegApp;

      use strict;
      use Catalyst qw/-Debug/;

      use CGI::FormManager;

      MyRegApp->config( name => 'MyRegApp',
                        fmgr => CGI::FormManager->new() );
      MyRegApp->setup;

      sub end : Global {
        my ($self, $c) = @_;
        $c->stash->{template} ||= "default.tt2";
        $c->forward('MyRegApp::View::TT') unless $c->res->output;
        die if $c->req->params->{die};
      }

      1;

    Registration is handled in a Catalyst controller package. It defines the
    form and loads it into the form manager (which it picks out of the
    application's configuration) as the package is loaded. Handling
    registration requests is done in the "user_details" action routine.

      package MyRegApp::Controller::Registration;

      use strict;
      use base 'Catalyst::Base';
      use CGI::FormManager qw(:element_decls :constraint_methods);

      my $form_defn = ...;  # (defined above)

      MyRegApp->config->{'fmgr'}->add_form(user_details => $form_defn);

      sub default : Private {
        my($self, $c) = @_;
        $c->forward('user_details');
      }

      sub user_details : Local {
        my($self, $c) = @_;
        my $form   = $c->config->{fmgr}->form('user_details');
        my $params = $c->request->params;
        my $stash  = $c->stash;

        if (!keys %$params) {
          $c->stash->{template} = "registration/new_user.tt2";
        }
        else {
          $form = $form->validate($params);
          if (!$form) {
            $c->log->debug("validation failed" );
            $c->log->debug(" missing:" . join(", ", $form->missing));
            $c->log->debug(" invalid:" . join(", ", $form->invalid));
            $c->stash->{template} = "registration/new_user_corrections.tt2";
            $c->stash->{message}  = <<EOS;
      There are problems with the information you supplied,
      please correct these and resubmit.
      EOS
          }
          else {
            $c->log->debug("validation passed");
            if ($form->confirmed_data) {
              # store the new user in the database
              $c->stash->{template} = "registration/welcome.tt2";
            }
            else {
              $c->stash->{template} = "registration/new_user_confirm.tt2";
            }
          }
          $c->stash->{form} = $form;
        }
      }

    The action method fetches the form object.

    If there were no parameters then it is a new request so it sets the
    template to be used to be the new user form page. Otherwise it gets the
    form manager to validate the parameters supplied against the form
    definition.

    If validation fails then the corrections template is used, otherwise we
    look and see if it was a confirmation form that was submitted. If we
    have come from the confirmation page then create the new user and
    display a welcome page, otherwise display the confirmation page.
    (Actually this routine doesn't included code for handling the user
    clicking on "edit" or "cancel" on the confirmation page, or the actual
    user creation, but you get the idea.)

    The general form of an action method is:

      sub myaction : Local {
        my ($self, $c) = @_;

        # Set up the template we are going to use (may be updated later)
        $c->stash->{template} = 'template-name';

        # Validate the submitted data against the form
        my $results = $fmgr->validate(form1 => $c->request);

        # Make the form-results object visible to the template
        $c->stash->{form} = $results;

        if ($results) {
          # The data is valid (NB evaluating $results in a boolean
          # context is the same as $results->success)

          if ($results->confirmed_data) {
            # do something with the data, according to whether
            # the user clicked "confirm", "edit" or "cancel"
            # (will need change the template used)
          }
          else {
            # data is OK, might want the user to confirm
            $c->stash->{template} = 'form1-confirm-template';
          }
        }
        elsif ($results->submitted) {
          # Data was received but is not valid -- do nothing and the
          # form will be redisplayed with appropriate error messages.
          # Note though that if $results->confirmed_data is true then
          # there may be a problem -- the confirmation form was
          # submitted but the data stored in hidden fields might
          # have been modified maliciously.
        }
        else {
          # no data was received so just display the initial form
        }
      }

OTHER FEATURES
  Lookup Elements
    These elements allow text to be generated by a callback routine.

  Repeating Groups of Fields
    Applications such as shopping carts require the ability to display
    repeating groups of form elements. This is the syntax I have come up
    with:

      cfm_repeating_group( elements =>
              [ cfm_hidden_field  ( name     => 'prodcode' ),
                cfm_lookup_element( name     => 'desc',
                                    header   => 'Description',
                                    value    => \&_row_description ),
                cfm_text_field    ( name     => 'qty',
                                    header   => 'Quantity'),
                cfm_lookup_element( name     => 'price',
                                    value    => \&_row_price ),
                cfm_lookup_element( name     => 'line_total',
                                    value    => \&_row_total,
                                    footers  => [ \&_sub_total,
                                                  \&_postage,
                                                  \&_tax,
                                                  \&_total ] )
              ],

    This will be rendered as a table of four columns (the hidden field will
    be prepended to the contents of the first column). The number of rows
    will be determined from the form data and there will be four footer rows
    for the calculated cells specified on the line total element.

OUTSTANDING ISSUES
    This software should be considered alpha quality. It has not been
    extensively tested and the exact details of the API have not been
    finalized. However there are applications using it that are in
    production use!

    This is a partial list of outstanding issues.

    *   expand the documentation

    *   complete the initialization of form field objects (defaults, cross
        population of validation/elements, radio field groups, etc)

    *   review the use of CSS classes on elements

    *   automatically generate JavaScript validation code (as
        "CGI::FormBuilder" does)

    *   add objects to group elements -- for example for side-by-side layout
        of billing and shipping addresses.

    *   expand the test suite

    *   design a proper Catalyst plugin

    *   have textual representation of form specification that can be read

SEE ALSO
    Data::FormValidator, Template, CGI::FormBuilder

AUTHOR
    Andrew Ford <A.Ford at ford-mason.co.uk>

COPYRIGHT
    This program is free software, you can redistribute it and/or modify it
    under the same terms as Perl itself.





More information about the Catalyst mailing list