The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

HTML::FormHandler::Manual::Tutorial - use FormHandler with Catalyst

DESCRIPTION

A tutorial for beginners to HTML::FormHandler

Using HTML::FormHandler with Catalyst

This tutorial demonstrates how you can use HTML::FormHandler to manage forms, validate form input, and interface your forms with the database.

Installation

Use CPAN to install HTML::FormHandler

Use the Tutorial application

We'll use the files that were created in the Catalyst::Manual::Tutorial, in order to concentrate on just the bits where HTML::FormHandler is useful. You can download a tar file of the tutorial files from the Catalyst code repository. (See Catalyst::Manual::Tutorial::Intro.)

Create an HTML::FormHandler form

Untar the tutorial and make a lib/MyApp/Form directory. In that directory create the file Book.pm.

   package MyApp::Form::Book;

   use HTML::FormHandler::Moose;
   extends 'HTML::FormHandler::Model::DBIC';

   has '+item_class' => ( default =>'Books' );
   has_field 'title' => ( type => 'Text' );
   has_field 'rating' => ( type => 'Integer' );
   has_field 'authors' => ( type => 'Multiple', label_column => 'last_name' );

   no HTML::FormHandler::Moose;
   1;

This is your Form class. The form initializes the 'item_class' to the source name of your DBIx::Class result class. The form's fields are defined with the 'has_field' sugar, or in a 'field_list'. The names of the fields should match a column, relationship, or other accessor in your DBIx::Class result class.

The basic fields have only a 'type', such as 'Text', or 'Integer'. These types are actually the names of HTML::FormHandler::Field classes. 'Text' and 'Integer' are types that are provided by HTML::FormHandler, in HTML::FormHandler::Field::Text and HTML::FormHandler::Field::Integer.

The 'Multiple' type will allow you to easily create a multiple select list from the 'authors' relationship. The 'label_column' attribute must be defined because the column in the 'authors' table which is used to create the select list does not have the default column name ('name').

Eventually you will want to create your own field classes, but for this simple form the default types are adequate.

Connect HTML::FormHandler to your controller

Edit lib/MyApp/Controller/Books.pm. Add use Moose:

    BEGIN {
       use Moose;
       extends 'Catalyst::Controller';
    }
    use MyApp::Form::Book;

Create an attribute to hold your form:

   has 'form' => ( isa => 'MyApp::Form::Book', is => 'rw',
       lazy => 1, default => sub { MyApp::Form::Book->new } );

Add Action to Display and Save the Form

In lib/MyApp/Controller/Books.pm add the following method:

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

        $c->stash( template => 'books/edit.tt2',
                   form => $self->form ); 

        # Validate and insert/update database 
        return unless $self->form->process( item_id => $book_id,
           params => $c->req->parameters,
           schema => $c->model('DB')->schema ); 

        # Form validated, return to the books list
        $c->flash->{status_msg} = 'Book saved';
        $c->res->redirect($c->uri_for('list'));
    }

This will handle both creating new books, and updating old books. If $book_id is undefined, then HTML::FormHandler will create a new book from your form. If you pass in a DBIx::Class row object instead of a primary key, you don't need to specify the schema.

Create a Template for the form

HTML::FormHandler provides only a simple sample 'render' routine to automatically create the HTML for your forms that you could use as a model for creating your own rendering routines. You could also use TT macros to do pretty sophisticated template generation. But for now, we'll stick to a straightforward TT template:

Open root/src/books/edit.tt2 in your editor and enter or copy the following:

   [% META title = 'Book Form' %]

   [% FOR field IN form.error_fields %]
     [% FOR error IN field.errors %]
       <p><span class="error" id="error">
          [% field.label _ ': ' _ error %] </span></p>
     [% END %]
   [% END %]


   <form name="[% form.name %]" 
         action="[% c.uri_for('edit', form.item_id) %]" 
         method="post">

   <p>
   [% f = form.field('title') %]
   <label class="label" for="[% f.name %]">[% f.label %]:</label>
   <input type="text" name="[% f.name %]" id="[% f.name %]" value="[% f.fif %]">
   </p>

   <p>
   [% f = form.field('rating') %]
   <label class="label" for="[% f.name %]">[% f.label %]:</label>
   <input type="text" name="[% f.name %]" id="[% f.name %]" %] value="[% f.fif %]">
   </p>

   <p>
   [% f = form.field('authors') %]
   <label class="label" for="[% f.name %]">[% f.label %]:</label>
   <select name="[% f.name %]" multiple="multiple" size="[% f.size %]">
     [% FOR option IN f.options %]
       <option value="[% option.value %]" 
         [% FOREACH selval IN f.fif %]
             [% IF selval == option.value %]selected="selected"[% END %]
         [% END %]>
       [% option.label | html %]</option>
     [% END %] 
   </select>
   </p>
    
   <input class="button" name="commit" type="submit" value="Submit" />

   </form>
    
   <p><a href="[% c.uri_for('list') %]">Return to book list</a></p>

Add a link to root/src/books/list.tt2 to allow you to edit an existing book, by changing the last <td> cell in the book list:

   <td>
      <a href="[% c.uri_for('delete', book.id) %]">Delete</a>|
      <a href="[% c.uri_for('edit', book.id) %]">Edit</a>
   </td>
      

Change the link to create a book at the bottom of the file:

    <p>
      <a href="[% c.uri_for('edit') %]">Create book</a>
    </p>

Test the HTML::FormHandler Create Form

Start up the server for MyApp:

    $ script/myapp_server.pl

(You'll need to login with test01/mypass if you're using the packaged tutorial.) Click the new "Create book" link at the bottom to display the form. Fill in the fields and click submit. You should be returned to the Book List page with a "Book saved" message.

Magic! A new book has been created and saved to the database with very little code in your controller.

Click on the 'edit' links, and edit the existing books. Changes should be saved and displayed properly. Try to add an alphabetic character to the rating field. You should get an error message.

Add additional attributes to your form's fields

We'll add a couple of 'label' attribute to the fields:

   has_field 'title' => ( type => 'Text', label => 'Title of a Book' );
   has_field 'rating' => ( type => 'Integer', label => 'Rating (1-5)' );
   has_field 'authors' => ( type => 'Multiple', label_column => 'last_name' );

    

HTML::FormHandler doesn't have built-in attributes for a submit button, but if you want to put one in your form to use in a template, go ahead.

   has '+submit' => ( default => sub { { name => 'submit', value => 'Submit' }});

In a template, you could then access this with:

   <input class="button" name="[% form.submit.name %]" 
        type="[% form.submit.type %]" value="Submit" />

If you want all your forms to have a submit button definition, you can put this in a base Form class. Or create a Moose::Role and group a number of similar functions together.

If you want a new attribute in your fields, it's very easy to add it to your custom Field classes.

   package MyApp::Form::Field::Extra;
   use Moose;
   extends 'HTML::FormHandler::Field';

   has 'my_attribute' => ( isa => Str, is => 'ro' );

   1;

Now if your Field classes inherit from this, you can have a 'my_attribute' attribute for all your fields. Or use a Moose role instead of inheritance.

HTML::FormHandler Validation

Now we'll add more validation to ensure that users are entering correct data.

Update the fields form file:

   has_field 'title' => ( type => 'Text', label => 'Title of a Book',
      required => 1, size => 40, min_length => 5 );
   has_field 'rating' => ( type => 'Integer', label => 'Rating (1-5)',
      required => 1, required_message => 'You must rate the book',
      range_start => 1, range_end => 5 );
   has_field 'authors' => ( type => 'Multiple', label_column => 'last_name',
      required => 1 );

We've made all the fields required. We added 'size' and 'min_length' attributes to the 'title' field. These are attributes of the 'Text' Field, which will use them to validate. We've added 'range_start' and 'range_end' attributes to the 'rating' field. Numbers entered in the form will be checked to make sure they fall within the defined range. (Another option would have been to use the 'IntRange' field type, which makes it easy to create a select list of numbers.)

Add customized validation

Usually you would create a Field class for validation that will be performed on more than one field, but it is easy to perform custom validation on a per-field basis.

This form doesn't really require any customized validation, so we'll add a silly field constraint. Add the following to the form:

  apply [ { check => sub { shift !~ /Rainbows/ },
            message => "The word \'Rainbows\' is not allowed in titles' }
        ];

You can also apply Moose constraints and transforms. Validation can also be performed in a form 'validate_<field_name' method, in a 'validate_model' routine, and in a custom field class. You can validate that the field is unique, or use a dependency list to make more fields required if one is updated.

Check out the validation

Restart the development server, login, and try adding books with various errors: title length less than 5 or more than 40, rating above 5, leaving out a particular field. Create a book with 'Rainbows' in the title.

You should get error messages for every error.

Database accessors

Although you can do database specific actions in a form class, you should limit this to actions that won't be required for anything except for your HTML forms. Other actions should be done in your DBIx::Class classes. HTML::FormHandler::Model::DBIC allows the use of custom accessors, making it easier to keep the code in the right place.

AUTHORS

Gerda Shank, gshank@cpan.org

COPYRIGHT

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