# $Id: /local/CPAN/Mango/lib/Mango/Form.pm 1644 2008-06-02T01:46:53.055259Z claco $ package Mango::Form; use strict; use warnings; BEGIN { use base qw/Class::Accessor::Grouped/; use Mango::Form::Results; use Mango::Exception qw/:try/; use Mango::I18N (); use FormValidator::Simple 0.17 (); use FormValidator::Simple::Constants (); use HTML::FormFu 0.02004 (); use Clone (); use YAML (); __PACKAGE__->mk_group_accessors( 'simple', qw/labels messages profile validator _form localizer _unique _exists/ ); } sub new { my $class = shift; my $args = shift || {}; my $source = $args->{'source'} || {}; my $self = bless { labels => $args->{'labels'} || {}, messages => $args->{'messages'} || {}, profile => $args->{'profile'} || [], validator => $args->{'validator'} || FormValidator::Simple->new, localizer => $args->{'localizer'} || \&Mango::I18N::translate, _unique => $args->{'unique'} || {}, _exists => $args->{'exists'} || {} }, $class; $self->parse($source); if ( $args->{'values'} ) { $self->values( $args->{'values'} ); } return $self; } sub action { my ( $self, $action ) = @_; if ($action) { $self->_form->action("$action"); } return $self->_form->attributes->{'action'}; } sub clone { my $self = shift; my $localizer = $self->localizer; my $params = $self->params; $self->params(undef); $self->localizer(undef); my $form = Clone::clone($self); $form->_form( $self->_form->clone ); $self->params($params); $self->localizer($localizer); return $form; } sub field { my $self = shift; my ( $name, $option, $value ) = @_; if ($value) { if ( ref $value eq 'ARRAY' ) { $self->_form->get_field( { name => $name } )->options($value); } else { $self->_form->get_field( { name => $name } )->value($value); } } return $self->_form->query->param(shift); } sub render { my $self = shift; my %args = @_; my $form = $self->_form; foreach my $arg ( keys %args ) { next if $arg =~ /values/i; $self->$arg( $args{$arg} ); } my $fields = $form->get_fields; if ( my $values = $args{'values'} ) { for ( my $i = 0 ; $i < scalar @{$values} ; $i++ ) { ## no critic if ( !$fields->[$i] ) { next; } $fields->[$i]->value( $values->[$i] ); } } $form->process; return $form->render; } sub values { my ( $self, $values ) = @_; if ($values) { foreach my $field ( keys %{$values} ) { $self->_form->get_field( { name => $field } ) ->value( $values->{$field} ); } } return map { $_->name, $self->_form->query->param( $_->name ) || undef } @{ $self->_form->get_fields }; } sub parse { my ( $self, $source ) = @_; my $config; if ( !ref $source ) { $config = YAML::LoadFile($source); } elsif ( ref $source eq 'HASH' ) { $config = Clone::clone($source); } else { Mango::Exception->throw('UNKNOWN_FORM_SOURCE'); } my $submit = delete $config->{'submit'} || 'BUTTON_LABEL_SUBMIT'; my $fields = delete $config->{'fields'}; my $indicator = '_submitted_' . ( $config->{'id'} || $config->{'name'} || 'noidorname' ); push @{$fields}, { $indicator => { type => 'Hidden', value => 1 } }; push @{$fields}, { '_submit' => { type => 'Submit', value => $submit } }; $config->{'indicator'} = $indicator; delete $config->{'sticky'}; delete $config->{'stylesheet'}; delete $config->{'javascript'}; if ( exists $config->{'name'} ) { $config->{'attributes'}->{'name'} = delete $config->{'name'}; } $self->_form( HTML::FormFu->new($config) ); $self->_parse_fields($fields); #$self->labels->{'submit'} = $config->{'submit'} || 'BUTTON_LABEL_SUBMIT'; $self->validator->set_messages( { '.' => $self->messages } ); return; } sub _parse_fields { my ( $self, $fields ) = @_; foreach ( @{$fields} ) { my ( $name, $field ) = %{$_}; my $label = 'FIELD_LABEL_' . uc $name; my $constraints = delete $field->{'constraints'}; my $errors = delete $field->{'messages'}; ## transform types my $type = delete $field->{'type'}; $type ||= 'Text'; $type =~ s/text/Text/i; $type =~ s/hidden/Hidden/i; $type =~ s/password/Password/i; $type =~ s/select/Select/i; $type =~ s/checkbox/Checkbox/i; if ( $type eq 'Checkbox' && exists $field->{'multiple'} && $field->{'multiple'} ) { $type = 'Checkboxgroup'; } if ( $type ne 'Submit' ) { if ( !exists $field->{'label'} && !exists $field->{'label_loc'} && !exists $field->{'label_xml'} ) { $field->{'label_loc'} = $label; } } ## migrate disabled $field->{'force_default'} = delete $field->{'force'}; $field->{'retain_default'} = $field->{'force_default'}; if ( exists $field->{'disabled'} ) { $field->{'attributes'}->{'disabled'} = delete $field->{'disabled'}; } ## migrate options/option labels my $options = delete $field->{'options'}; my $labels = delete $field->{'labels'}; if ($options) { $field->{'options'} = [ map { [ $_ => $labels->{$_} ] } @{$options} ]; } ## delete unknowns delete $field->{'selectname'}; delete $field->{'multiple'}; $self->_form->element( { name => $name, type => $type, %{$field} } ); $self->_parse_constraints( $name, $constraints, $errors ); } return; } sub _parse_constraints { my ( $self, $name, $constraints, $errors ) = @_; if ($constraints) { my @constraints; my @additional; push @{ $self->profile }, $name; foreach my $constraint ( @{$constraints} ) { my ( $cname, @args ) = split /, ?/, $constraint; $cname = uc $cname; if ( $cname =~ /(NOT_)?SAME_AS/ ) { my $mname = 'CONSTRAINT_' . uc $name . '_' . $cname . '_' . uc $args[0]; $self->messages->{$mname}->{ ( $1 || '' ) . 'DUPLICATION' } = $mname; push @additional, { $mname => [ $name, @args ] }, [ ( $1 || '' ) . 'DUPLICATION' ]; } else { if ( $cname eq 'UNIQUE' && !$self->unique($name) ) { if ( !scalar @args ) { push @args, $name; } $self->unique( $name, sub { return FormValidator::Simple::Constants::FALSE; } ); } if ( $cname eq 'EXISTS' && !$self->exists($name) ) { if ( !scalar @args ) { push @args, $name; } $self->exists( $name, sub { return FormValidator::Simple::Constants::FALSE; } ); } $self->messages->{$name}->{$cname} = $errors->{$cname} || ( 'CONSTRAINT_' . uc $name . '_' . $cname ); push @constraints, scalar @args ? [ $cname, @args ] : $cname; } } push @{ $self->profile }, \@constraints, @additional; } return; } sub params { my $self = shift; if (@_) { $self->_form->query(shift); } return $self->_form->query; } sub exists { my ( $self, $field, $code ) = @_; if ( ref $code eq 'CODE' ) { $self->_exists->{$field} = $code; } return $self->_exists->{$field}; } sub unique { my ( $self, $field, $code ) = @_; if ( ref $code eq 'CODE' ) { $self->_unique->{$field} = $code; } return $self->_unique->{$field}; } sub validate { my $self = shift; $self->validator->results->clear; $self->_form->process; ## ugly. this will go when I move to Reaction :-) local *FormValidator::Simple::Validator::UNIQUE = sub { my ( $i, $params, $args ) = @_; my $value = $params->[0]; my $field = $args->[0]; my $result = $self->unique($field)->( $self, $field, $value ); return $result ? FormValidator::Simple::Constants::TRUE : FormValidator::Simple::Constants::FALSE; }; local *FormValidator::Simple::Validator::EXISTS = sub { my ( $i, $params, $args ) = @_; my $value = $params->[0]; my $field = $args->[0]; my $result = $self->exists($field)->( $self, $field, $value ); return $result ? FormValidator::Simple::Constants::TRUE : FormValidator::Simple::Constants::FALSE; }; ## bah! why can't we just get $form->values? my %values = $self->values; my $results = $self->validator->check( \%values, Clone::clone( $self->profile ) ); my $messages = $results->messages('.'); my @errors; foreach my $message ( @{$messages} ) { push @errors, $self->localizer->( split /, ?/, $message ); } return Mango::Form::Results->new( { _results => $results, errors => \@errors } ); } sub submitted { my $self = shift; $self->_form->process; return $self->_form->submitted; } sub AUTOLOAD { my $self = shift; my ($method) = ( our $AUTOLOAD =~ /([^:]+)$/ ); return if $method =~ /(DESTROY)/; return $self->_form->$method(@_); } 1; __END__ =head1 NAME Mango::Form - Module representing an input form =head1 SYNOPSIS my $form = Mango::Form->new({ source => 'path/to/some/config.yml' }); =head1 DESCRIPTION Mango::Form renders forms using CGI::FormBuilder and validates data using FormValidator::Simple, all from a single configuration format. =head1 FORM FILE FORMAT The form file format is YAML. The top level options are passed directly to CGI::FormBuilder. The collection of C are parsed out into FormBuilder field specs and sent to FormBuilder. The C are FormValidator::Simple constraint names. --- name: form_name method: POST javascript: 0 stylesheet: 1 sticky: 1 submit: LABEL_CREATE fields: - sku: type: text size: 25 maxlength: 25 constraints: - NOT_BLANK - LENGTH, 1, 25 - UNIQUE - name: type: text size: 25 maxlength: 25 constraints: - NOT_BLANK - LENGTH, 1, 25 - description: type: text size: 50 maxlength: 100 constraints: - NOT_BLANK - LENGTH, 1, 100 - price: type: text size: 25 maxlength: 12 constraints: - NOT_BLANK - DECIMAL, 9, 2 - tags: type: textarea =head2 constraints Each constraint in the constraints collection is the name of a FormValidator::Simple validation command. You can pass options to that command by simply adding to that line separated by commas. The C constraint is specific to Mango::Form. When specified, it will run the code reference associated with the current field. You can use a different field name by passing it as another option: - sku: type: text size: 25 maxlength: 25 constraints: - NOT_BLANK - LENGTH, 1, 25 - UNIQUE, part_number By default, all UNIQUE constraints will fail, unless you tell the form how to validate that field. You can do this by calling L: $form->unique('sku', sub { my ($self, $field, $value) = @_; ...determine if sku exists return $exists ? 1 : 0; }); =head2 messages The messages default to FIELDNAME_CONSTRAINTNAME, which is then localized. In the example above, the error message returned when the sku was blank would be SKU_NOT_BLANK. When the price failed the decimal check, PRICE_DECIMAL is returned, etc. You can override these defaults to use your own message key, or provide a complete text message itself using the C collection and assigning the new message to the same constraint name: - sku: type: text size: 25 maxlength: 25 constraints: - NOT_BLANK - LENGTH, 1, 25 - UNIQUE, part_number messages: NOT_BLANK: "sku cannot be blank" LENGTH: "sku is too long" UNIQUE: "sku already exists" =head2 localization When running by itself, messages and field labels are localized using Mango::I18N. While running under the Mango Catalyst controllers, $c->localize is used to localize the messages and labels, which will use MyApp::I18N or MyApp::L10N. You can also use your own localizer by passing a code reference into the C property: $form->localizer(sub { my $message, $args) = @_; ...localize magic }); =head1 CONSTRUCTOR =head2 new =over =item Arguments: \%options =back Creates a new Mano::Form object. The following options are available: my $form = Mango::Form->new({ source => 'thisform.yml', unique => { field => \&field_is_unique }, localizer => sub { MyApp->localize(@_); } }); =over =item source A string containing the path to the config file, or a hash reference containing the same configuration data. =item validator An instance of the validator to be used. This is an instance of FormValidator::Simple by default. Using anything else at this time will probably not work. :-) =item localizer A code reference to be used to localize the field labels, buttons and messages. =item exists A hash reference containing methods to be used to determine if s fields values already exists. =item unique A hash reference containing methods to be used to determine field value uniqueness. =item values A hash reference containing the default form field values. =back =head1 METHODS =head2 action =over =item Arguments: $action =back Gets/sets the action for the current form. =head2 clone Creates and returns a clone of the current form. =head2 exists =over =item Arguments: $field, \&code =back Gets/sets the code reference to be used to determine if a fields value already exists. $form->exists('field')->($self, 'field', 'value'); $form->exists('field', sub { my ($self, $field, $value) = @_; ...exists magic... }; =head2 field =over =item Arguments: $name or %options =back Gets/sets a form fields information and other options. my $field = $form->field('sku'); $form->field(name => 'sku', options => [...]); See L for more information. =head2 localizer =over =item Arguments \&sub =back Gets/sets the code reference used to localize form field labels, buttons and messages. $form->localizer( sub { my $self = shift; return MyApp->localize(@_); } ); =head2 params =over =item Arguments: $object =back Gets/sets the object to read params from. This can be a CGI object, the Catalyst::Request object, or any other object that supports the param() method. $form->params($r); =head2 parse =over =item Arguments: $source =back Parses the specified configuration and creates the appropriate forms, fields, constraints and messages. $form->parse('/myform.yml'); C can be either a string containing the configuration file name, or a hash reference containing the same data structure. =head2 render Returns the html source for the current form. =head2 submitted Returns true if the form has been submitted. if ($form->submitted) { ... }; =head2 unique =over =item Arguments: $field, \&code =back Gets/sets the code reference to be used to determine if a field value is unique or not. $form->unique('field')->($self, 'field', 'value'); $form->unique('field', sub { my ($self, $field, $value) = @_; ...unique magic... }; =head2 validate Validates the current for values against the constraints and returns a Mango::Form::Results instance. my $results = $form->validate; if ($results->success) { ...save to db... } else { my $errors = $results->errors; ... }; =head2 values =over =item Arguments: \%values =back Gets/sets the current form fields values. my %values = $form->values; $form->values({ field11 => 'Foo', field2 => 2 }); =head1 SEE ALSO L, L =head1 AUTHOR Christopher H. Laco CPAN ID: CLACO claco@chrislaco.com http://today.icantfocus.com/blog/