HTML::FormFu talk for Sydney PM

I am in the process of authoring a talk for Sydney PM as an intro to HTML::FormFu (via Catalyst). The draft of which follows, to which I would welcome feedback and suggestions.


HTML::FormFu

Efficient web forms.

So you can leave at 5pm each day.


Web frameworks like Catalyst, Mojolicious & Dancer take care of repetitive things in web app development such as:

  • Connecting your code to an environment (modperl1/2, cgi, fastcgi, nginx_perl etc. ok * Plack does a lot of this)
  • Mapping uri requests to code
  • Authentication
  • Authorization
  • Sessions
  • Connecting to databases & ORM's
  • Serialization/Deserialization (JSON, XML etc)
  • Templating (built on TT, Alloy etc)
  • Logging/Debugging levels
  • SiteMaps, Caching, Browser detection
  • Code Bootstrappers

Whats missing is form generation and validation.

It's very rare not to have form's in any software - business applications have lots of them.

They are very boring and repetitive, prone to mistakes and bugs as a result.

Data types are frequenly the same things: names, addresses, emails, phone numbers, etc.

Form state often isn't tracked properly, making forms vulernable to attacks / mistakes.

Reliance only on client side javascript to enforce form data integrity is insufficient (but fun!)


HTML::FormFu is a solution that implements a framework and lots of helpers to handle forms.

Simple define your form using a 'DSL' of sorts, although it uses Config::Any. So maybe its not a DSL?

Then plug code in to the various events where things need to happen, like successful validation or errors.


Alternatives deserve a mention:

  • CGI::FormBuilder (even has a Padre plugin)
  • HTML::FormHandler
  • WWW::Form?
  • Others?

Try them, see what suits you!


HTML::FormFu is a stand alone module, but in this talk I will present it via its Catalyst::Controller::HTML::FormFu module.

Find them all on the CPAN:

cpanm HTML::FormFu
cpanm Catalyst::Controller::HTML::FormFu

Examples in this talk are taken from PhotoGame.

  • A more than trivial but still small Catalyst app which I authored
  • Basically a "hot or not" voting game clone
  • Used to encourage people to take and upload photos at our LANparty events (so we can use them later!)
  • Provides a 'tournament' for photography enthusiasts
  • Catalyst + MySQL using HTML::FormFu and Imager
  • https://github.com/djzort/PhotoGame

  • People can register and upload photos
  • Uploads are saved to disk, then resized in a cron batch
  • Photos can be voted on (A>B or B>A) once per user/ip
  • Provides a leader board
  • Gradually a winner emerges
  • Runs just fine on an early Intel Atom

You can use the helper (See also perldoc Catalyst::Helper::HTML::FormFu)

script/myapp_create.pl HTML::FormFu forms

Which will create the root/forms directory

Basically, your controller can be converted to using FormFu by changing the extends line to this..

BEGIN { extends 'Catalyst::Controller::HTML::FormFu' }

Everything will work as normal, until you add the 'FormConfig' attribute to a subroutine (or do it manually... as explained in the pod)

HTML::FormFu can output either HTML or Template Toolkit streams - which are then processed by a TT based view.


Form Validation

Form field input goes through 4 stages, in order, and as long as the result of each stage is successful:

  • filters - e.g. remove leading/trailing spaces
  • constraints - low-level checks ("is this a number?", "is this a valid e-mail address?")
  • inflators - turn a value into an object, ready to be passed to validators
  • validators - application-level checks ("is this username unique?")

(http://wiki.catalystframework.org/wiki/howtos/forms/formfu.view)


Then you have two options, use $c->stash->{form} object methods or 'Special Action Name' subs.

In either case you add the 'FormConfig' sub attrbute. Lets use magical 'Special Action Name' subs to make a login form.

Here is the perl code in Controller/Root.pm...

sub login : Path('login') : Args(0) : FormConfig {
  my ( $self, $c ) = @_;
  if ( my $me = $c->session->{iam} ) {
    $c->stash->{message} = 
        sprintf( 'You\'re already logged in as %s, go play!', 
        $me->{full_name} ); $c->detach('message');
  }
}

sub login_FORM_RENDER {
}
sub login_FORM_NOT_SUBMITTED {
}

sub login_FORM_VALID {
  my ( $self, $c ) = @_;
  if ( my $me = $c->model('DB')
       ->check_user( $c->req->param('username'), $c->req->param('password') ) )
  {
      $c->session->{iam} = $me;
      $c->stash->{message} =
          sprintf( 'Welcome %s, lets play!', $me->{full_name} );
          $c->detach('message');
   }
   else {
      $c->stash->{error} = 'Failed to log in';
   }
}

sub login_FORM_NOT_VALID {
  my ( $self, $c ) = @_;
  $c->stash->{error} = 'Failed to log in';
}

Then define the form based on the sub name. FormFu uses Config::Any - So use a format that suits you. (I have no idea why I used YAML)

File is root/forms/login.yml

---
action: /login
indicator: submit

elements:
 - type: Fieldset
   name: login
   elements:
      - type: Src
        content_xml: "<legend>Login</legend>"
      - type: Text
        name: username
        label: "Username:"
        add_label_attributes:
            class: auto
        attributes:
          id: username
          title: Enter your username
        constraints:
          - Required
      - type: Password
        name: password
        label: "Password:"
        add_label_attributes:
            class: auto
        attributes:
          id: password
          title: Enter your password
        constraints:
          - Required
      - type: Submit
        name: submit
        value: Login

    constraints:
      - SingleValue


The format/syntax more or less follows what the HTML will look like.

  • <form> encompasses a <fieldset> (if you like fieldsets)
  • I had to use the Src tag to manually add xml (html) to add a <legend> tag
  • <text> for username and password then a submit button
  • These are all implemented in HTML::FormFu::Element::* modules

Various attributes are defined for each. id, title, class etc

Constraints are how we tell FormFu how data should be validated These are all implemented in HTML::FormFu::Constraint::* modules

Required means a field must have a value. SingleValue ensures multiple values are not submitted.


Out of the box, you get just about everything you can imagine and you can easily add more.

  • text, textarea, hidden, password, radio, select, checkbox, image, file, submit/reset buttons.
  • derivatives such as email, date, datetime, url
  • html elements like hr, fieldset, Src

  • constraints for min, max, integer, length, min & max length, number, range,

  • regex, ascii, email, datetime, word, printable characters,
  • set, singlevalue, bool, equal

plus many others (homework for you)


Here is a more complex example in the registration form in Controller/Root.pm...

 sub register : Path('register') : Args(0) : FormConfig {
  my ( $self, $c ) = @_;
  unless ( $c->model('DB')->get_setting('registration_open') ) {
    $c->stash->{message} = 'Registrations are closed';
    $c->detach('message');
  }
 }

 sub register_FORM_RENDER {
 }
 sub register_FORM_NOT_SUBMITTED {
 }

 sub register_FORM_VALID {
  my ( $self, $c ) = @_;
  $c->model('DB')->create_photographer(
    username => $c->req->param('username'),
    password => $c->req->param('password'),
    full_name => $c->req->param('full_name'),
    email_addr => $c->req->param('email_addr'),
    creation_ip => $c->req->address,
  );
  $c->stash->{message} = 'user created';
  $c->detach('message');
 }

 sub register_FORM_NOT_VALID {
  my ( $self, $c ) = @_;
  $c->stash->{error} = 'Submission failed, see comments above';
 }


config is root/forms/register.yml

---
action: /register
indicator: submit
output_processors:
 - Indent

elements:
  - type: Fieldset
    name: register
    elements:
      - type: Src
        content_xml: "<legend>Register to Compete!</legend>"

      - type: Text
        name: full_name
        label: "Full Name:"
        attributes:
          id: fullname
          title: Full Name
        constraints:
          - type: Required

      - type: Text
        name: email_addr
        label: "Email:"
        attributes:
          id: emailaddr
          title: Email Address
        constraints:
          - type: Required
          - type: Email

      - type: Hr

      - type: Text
        name: username
        label: "Username:"
        attributes:
          id: username
          title: Username
        constraints:
          - type: Required
          - type: Length
            min: 2
            max: 255
        validators:
          - '+PhotoGame::Validator::UniqueUsername'

      - type: Password
        name: password
        label: "Password:"
        attributes:
          id: password
          title: Password
        constraints:
          - type: Required
          - type: Length
            min: 6
            max: 255
          - type: Equal
            others: repeat-password

      - type: Password
        name: repeat-password
        label: "Repeat:"
        attributes:
          id: repeat-password
          title: Repeat Password

      - type: Submit
        name: submit
        value: Go

 constraints:
  - SingleValue


The registration form has lots of examples of what can be used.

  • Required constraing in all cases
  • Email constraint used on email field
  • Min and Max lenfth used on username and password field
  • Repeat-password used to prompt twice for the password and ensure sameness
  • Src and Hr used for a bit of formatting
  • Output processor of Indent makes the HTML look pretty (adds cpu, but helps with for debugging html)
  • Custom constraint '+PhotoGame::Validator::UniqueUsername' which checks the DB for username uniqueness

Calls in to the model to check if the username is already used, dies with a HTML::FormFu::Excepton object

PhotoGame/Validator/UniqueUsername.pm

 package PhotoGame::Validator::UniqueUsername;
 use strict; use warnings;
 use base 'HTML::FormFu::Validator'; # use parent might be better?

 sub validate_value {
  my ( $self, $value, $params ) = @_;
  my $c = $self->form->stash->{context};
  if ($c->model('DB')->username_taken($value)) {
    die HTML::FormFu::Exception::Validator->new({
    message => 'username taken',
    });
  }
  return 1
 }
 1

As you can see its very easy to add/edit/remove form elements. Its easy enough that junior or non-programmers can get it right.

Lots and lots of boring code is gone, which is still good even if you use YAML.

Web Designers can whip up form modifications for clients and finalize them, before handing them to developers to tie in to the database (an ORM may avoid that step entirely)

Using common type constraints will give you consistency through out your software (instead of email being a different regex in different locations, sometimes with MX look ups)

HTML::FormFu may add more CPU usage, depending on how good/bad your coding is.


You can do cool AutoCRUD stuff with FormFu and DBIC. Maybe I can add that in the future!

HTML::FormFu::ExtJS exists which promises to generate Javascript forms for you based on ExtJS.


Questions?


ENDE


2 Comments

Form::Diva is a simple html5 form element generator that I wrote. It makes generating forms very easy and about as compact as I could imagine it being, but all it does is generate form elements for easy assembly in the view.

https://metacpan.org/pod/Form::Diva

Leave a comment

About Dean

user-pic I blog about Perl.