NAME

CGI::Application::Plugin::GenVal - Generate input forms with client/server validation

DESCRIPTION

This module aims to make the tricky task of setting up forms with accompanying client/server side validation much simpler. Once setup rather than editing HTML, javascript and perl all you need to do is edit one central YAML file. It uses Data::Formvalidator and a modified version of Data.FormValidator.js. It also utilizes CGI::Application::Plugin::YAML if available, otherwise loads YAML::Any. Newer versions of Data::FormValidator use compiled regexps and new style contraint methods. This broke compatibility when converting the profile to JavaScript. This module fixes that issue by having the constraints in YAML which are then compiled into the new format for Data::FormValidator and converted to the old format for Data.FormValidator.js. Allowing you to get the best from both worlds.

This is alpha, anything could change dramatically!!!

SYNOPSIS

This module allows you to consolidate your form generation, client and server side validation into a central YAML schema file.

use CGI::Application::Plugin::GenVal qw( :std );

Create Data::FormValidator Perl and JavaScript profiles, return YAML schema:-

( $dfv_profile, $js_profile, $yaml_schema ) = $self->GenVal->gen_dfv( {
        schema => 'formschema.yml.pl',
        form   => 'form1',
        required_hash => { 'field_name_1' => 1, 'field_name_2' => 1 },
        required => [ 'field_name_3', 'field_name_3' ],
        msgs => {
            missing       => 'Required',
            invalid       => 'Invalid',
            constraints   => {
                'eq_with' => 'Must match',
            },
        },
    } );

Generate input field:-

$self->GenVal->gen_input( {
        field => $field,
        value => $value,
        details => $yaml_schema->{field_input}->{$field},
        style => $yaml_schema->{field_input_style},
        error => { type => 'good' },
    } );

Example code... In YAML file:-

---
field_text:
  name: Name
  website: Website
  email: Email
  email-confirm: Confirm Email
  phone: Phone Number
field_input:
  name:
    max: 24
    size: 30
    type: text
    constraint: /^(?:[\w\- \,\.]*){0,24}$/
    required: yes
  website:
    max: 128
    size: 134
    type: text
    constraint: /^https?:\/\/[\w\- .]*$/
    required: no
  email:
    max: 64
    size: 70
    type: text
    required: yes
    constraint:
    - /^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i
    - constraint: method FV_eq_with('email-confirm')
  email-confirm:
    max: 64
    size: 70
    type: text
    required: yes
    constraint:
    - /^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i
    - constraint: method FV_eq_with('email')
  phone:
    max: 20
    size: 26
    type: text
    constraint: /^[0-9 \-\+\(\)]*$/i
    required: no
field_input_style:
  all:
    normal:
      style:
        background: '#cdffbe'
        border: '1px solid #28bc40'
        border-color: '#009d16 #00e921 #00e921 #009d16'
    good:
      style:
        background: '#cdffbe'
        border: '1px solid #28bc40'
        border-color: '#009d16 #00e921 #00e921 #009d16'
    bad:
      style:
        background: '#ffbebe'
        border: '1px solid #d81f1f'
        border-color: '#940000 #e20404 #e20404 #940000'
signup:
  name
  website
  email
  email-confirm
  phone

In HTML Template:-

<form action="cgiapp.cgi" method="post" onSubmit="return signupValidate(this);" name=signup>
    <input type=hidden name=rm value="processform" />
    <table border=0 cellpadding=4 cellspacing=0>
        <tmpl_loop name=signup_fields>
            <tr>
                <td>
                    <tmpl_if name=field_required><b>*</b></tmpl_if>
                    <tmpl_var name=field_text>
                    <div id="error_<tmpl_var name=field_name>">
                        <b><tmpl_var name=field_error></b>
                    </div>
                </td>
                <td><tmpl_var name=field_input></td>
            </tr>
        </tmpl_loop>
    </table>
    <input type=submit name=submit value="Submit" />
</form>

In your CGI::Application runmode:-

use Data::FormValidator;

### Display and process form
sub processform {
    my $self = shift;

    ### Load dfv profiles and schema
    my ( $dfv_profile, $js_profile, $yaml_schema ) = $self->GenVal->gen_dfv( {
        schema => 'signup.yml.pl',
        form   => 'signup',
        msgs => {
            missing       => 'Required',
            invalid       => 'Invalid',
            constraints   => {
                'eq_with' => 'Must match',
            },
        },
    } );

    ### Check if we've received data and validate
    my $errorhash;
    if ( $self->query->param( 'submit' ) ) {
        my $results = Data::FormValidator->check( $self->query, $dfv_profile );
        
        ### get error details
        $errorhash = $results->msgs;
        
        ### Forword to next runmode if no errors
        unless ( keys %$errorhash ) {
            return $self->forward('saveform');
        }#else
    }#if

    ### Prepare form input field style
    my $errordetails = {
        type => 'normal',
    };

    ### Loop through fields for this form
    my @signup_fields;
    foreach my $field ( @{ $yaml_schema->{signup} } ) {

        ### Style fields to good or bad if input did not validate
        if ( $errorhash ) {
            if ( $errorhash->{"err_$field"} ) {
                $errordetails = {
                    type => 'bad',
                    text => $errorhash->{"err_$field"},
                };
            }#if
            else {
                $errordetails->{type} = 'good';
            }#else
        }#if

        ### Fix required for tmpl_if
        ### Needed because we allow people to set required to 'no'
        my $required = 0;
        if ( $yaml_schema->{field_input}->{$field}->{required} && lc( $yaml_schema->{field_input}->{$field}->{required} ) ne 'no' ) {
            $required = 1;
        }#if

        ### Generate input field and prepare for template
        my $value = $self->query->param( $field );
        push( @signup_fields, {
            field_name     => $field,
            field_required => $required,
            field_text     => $yaml_schema->{field_text}->{$field},
            field_error    => $errorhash->{"err_$field"},
            field_input    => $self->GenVal->gen_input(
                $field,
                $value,
                $yaml_schema->{field_input}->{$field},
                $yaml_schema->{field_input_style},
                $errordetails,
            ),
        });
    }#foreach
    
    my $template = $self->load_tmpl( 'form.html', die_on_bad_params => 0 );
    $template->param(
        {
            js_profile        => $js_profile,
            signup_fields     => \@signup_fields,
        }
    );

    return $template->output();
}#sub

Object

GenVal

This is the only method returned. It is in fact an object with the following methods.

Methods

gen_input

This method generates the input fields. The following describes it's input parameters which must be passed in as a hash reference.

style

The fields can have different styles, such as normal (used the first time form is displayed), good (if the input value validated) and bad (if the input value failed validation). These styles can be applied to specific field types, or as the default for all field types by defining a key called all. The style parameter should be passed in as a hash reference in the format:-

$style = {
    all => {
        normal => {
            style => {
                background   => '#cdffbe',
                border       => '1px solid #28bc40',
                border-color => '#009d16 #00e921 #00e921 #009d16',
            },
        },
        good => {
            style => {
                background   => '#cdffbe',
                border       => '1px solid #28bc40',
                border-color => '#009d16 #00e921 #00e921 #009d16',
            },
        },
        bad => {
            style => {
                background   => '#ffbebe',
                border       => '1px solid #d81f1f',
                border-color => '#940000 #e20404 #e20404 #940000',
            },
        },
    },
    select => { ### Overload 'all' defaults for 'select' field types
        normal => {
            style => {
                background   => '#ffffff',
            },
        },
};

This is usually stored in your YAML schema rather than your Perl code.

field_input_style:
  all:
    normal:
      style:
        background: '#cdffbe'
        border: '1px solid #28bc40'
        border-color: '#009d16 #00e921 #00e921 #009d16'
    good:
      style:
        background: '#cdffbe'
        border: '1px solid #28bc40'
        border-color: '#009d16 #00e921 #00e921 #009d16'
    bad:
      style:
        background: '#ffbebe'
        border: '1px solid #d81f1f'
        border-color: '#940000 #e20404 #e20404 #940000'
  select:
    normal:
      style:
        background: '#ffffff'

field

This is the field name

value

This is the field value (if any)

details

You must define the details of your form field, such as type, size, etc. The details parameter must be a hash reference with keys of type (values text, password, select, custom) max (used for text and password field types) size (field display size) source (data source for some types).

These are generally stored in your YAML schema:-

field_input:
  name:
    max: 24
    size: 30
    type: text
    constraint: /^(?:[\w\- \,\.]*){0,24}$/
    required: yes
  website:
    max: 128
    size: 134
    type: text
    constraint: /^https?:\/\/[\w\- .]*$/
    required: no

Notice how the schema includes extra keys for constraint and required. They are not used here, but do not cause any problems.

For select field types, you must provide a data source. This is passed in value, and expected to return a hash reference of labels (key being the option value, value being the option label), an array reference of option values (to get the order) and scalar of the default value. The source must be defined as a string in the form:-

source => 'sub self SUBNAME', ### Object method

Or

source => 'sub SUBNAME', ### Subroutine

Which are translated to:-

( $labels, $values, $default ) = $self->SUBNAME( $value );

And

( $labels, $values, $default ) = SUBNAME( $value );

For custom field types, you must also provide a data source. This is passed in value and a CGI.pm style tag attribute. It's expected to return the HTML for the form field. The source must be defined as a string in the form:-

source => 'sub self SUBNAME', ### Object method

Or

source => 'sub SUBNAME', ### Subroutine

Which are translated to:-

$html = $self->SUBNAME( $value, $style );

And

$html = SUBNAME( $value );

Examples in YAML:-

field_input:
  month:
    type: select
    source: sub self month_select
    constraint: /^[\w]*$/
  timezone:
    type: custom
    source: sub create_timezones
    required: yes
    constraint: /^[\w\-\/]*$/

error

This parameter is used to define the style used, and provide any error text. It must be a hash reference with keys type to match the style type (normal, good, bad) text the error message if any

$errorhash = {
    type => 'normal'
};

$errorhash = {
    type => 'bad',
    text => 'Field required',
};

gen_dfv

This method generates the DFV profiles for Perl and JavaScript from YAML schema. It returns both profiles and the YAML schema. The YAML schema can be passed in, or you can pass a filename instead. The following describes it's input parameters which must be passed in as a hash reference.

prefix

Set a prefix for field error messages. This is the same as DFV {msgs}->{prefix}. Defaults to 'err_' to make 'err_FIELDNAME' => 'Required', etc.

any_errors

Sets a flag in the results if any errors were returned. Same as DFV {msgs}->{any_errors}. Defaults to 'some_errors'.

required

List of required fields. Defaults to empty. This list is added to from the YAML schema field_input settings.

required_hash

Hash reference of required fields. This is useful if you have your required fields defined by a configuration file. Can be in the form:-

{required_hash}->{fieldname}->{required} = 1;

or

{required_hash}->{fieldname} = 1;

constraints_loaded

Hash list of constrant methods from Data::FormValidator::Constraints that have already been loaded. This is useful if you have multiple forms to save importing them again.

schema

This is either your pre-loaded YAML schema, or the file location. Such as:-

{schema} => $LoadedYAML,

or

{schema} => 'myform.yml.pl',

Schema must be in the format:-

---
field_text:
  FIELDNAME: DESCRIPTION
  FIELDNAME2: DESCRIPTION2
field_input:
  FIELDNAME:
    constraint: /^(?:[\w\- \,\.]*){0,24}$/
    required: yes
  FIELDNAME2:
    constraint: /^https?:\/\/[\w\- .]*$/
    required: no
FORMNAME:
  FIELDNAME
  FIELDNAME2

You must have a key called field_input that contains a hash of the fieldnames, which in turn contain a hash of the constraint and if the field is required. The FORMNAME list if used to generate the optional fields for this form.

form

The name of this form (required). Naming the form allows you to have multiple forms on the same page.

msgs

This is simply passed through to the DFV {msgs} key. If you set {msgs}->{prefix} or {msgs}->{any_errors} they will override the {prefix} and {any_errors} described above.

About Constraints

Newer versions of Data::FormValidator use compiled regexps and new style contraint methods. This broke compatibility when converting the profile to JavaScript. This module fixes that issue by having the constraints in YAML which are then compiled into the new format for Data::FormValidator and converted to the old format for Data.FormValidator.js. Allowing you to get the best from both worlds.

All constraints must be in a key called 'constraint'. This is loaded to 'constraint_methods' for Perl and 'constraints' for JavaScript.

You can define multiple constraints by having the 'constraint' key point to an array, such as:-

field_input:
  FIELDNAME:
    constraint:
    - /^[a-z]*$/i
    - /^[A-C]*$/

This would generate Perl of:-

$dfv->{constraint_methods}->{FIELDNAME} = [
    qr/^[a-z]*$/i
    qr/^[A-C]*$/
];

JavaScript of (well perl version of javascript):-

$dfv->{constraints}->{FIELDNAME} = [
    '/^[a-z]*$/i'
    '/^[A-C]*$/'
];

If JavaScript doesn't support the regexp such as those ending in or /s or /x, then the constraint isn't loaded into JavaScript:-

field_input:
  FIELDNAME:
    constraint:
    - /^[a-z]*$/i
    - /^[A-C]*$/s

This would generate Perl of:-

$dfv->{constraint_methods}->{FIELDNAME} = [
    qr/^[a-z]*$/i
    qr/^[A-C]*$/s
];

JavaScript of (well perl version of javascript):-

$dfv->{constraints}->{FIELDNAME} = '/^[a-z]*$/i';

You can also use the new DFV built in methods, such as FV_eq_with. This is downgraded to an old style version for JavaScript:-

field_input:
  email:
    required: yes
    constraint:
    - /^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i
    - method FV_eq_with('email-confirm')
  email-confirm:
    required: yes
    constraint:
    - /^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i
    - method FV_eq_with('email')

This would generate Perl of:-

$dfv->{constraint_methods} =
    'email' => [
        qr/^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i,
        \&FV_eq_with( 'email-confirm' ), ### Similar, FV_eq_with actually returns a subroutine
    ],
    'email-confirm' => [
        qr/^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i,
        \&FV_eq_with( 'email' ), ### Similar, FV_eq_with actually returns a subroutine
    ],

JavaScript of (well perl version of javascript):-

$dfv->{constraints} =
    'email' => [
        '/^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i',
        {
            constraint => 'FV_eq_with',
            params => [ 'email-confirm' ],
            name => 'eq_with',
        },
    ],
    'email-confirm' => [
        qr/^(([a-z0-9_\.\+\-\=\?\^\#]){1,64}\@(([a-z0-9\-]){1,251}\.){1,252}[a-z0-9]{2,4})$/i,
        {
            constraint => 'FV_eq_with',
            params => [ 'email' ],
            name => 'eq_with',
        },
    ],

About JavaScript

The returned JavaScript code includes a function named 'formValidate' (where form is form name) encased in a <SCRIPT> tag ready to be parsed into your HTML page. You also need to update your <FORM> tag to include:-

onSubmit="return formValidate(this);"

Such as:-

<form action="cgiapp.cgi" method="post" onSubmit="return formnameValidate(this);" name=formname>

Export groups

Only an object called GenVal is exported.

:all exports:-

GenVal

:std exports:-

GenVal

Thanks to:-

Data::FormValidator

Come join the bestest Perl group in the World!

Bristol and Bath Perl moungers is renowned for being the friendliest Perl group in the world. You don't have to be from the UK to join, everyone is welcome on the list:- http://perl.bristolbath.org

AUTHOR

Lyle Hopkins ;)