package HTML::FormFu; use strict; use base 'HTML::FormFu::base'; use HTML::FormFu::Attribute qw/ mk_attrs mk_attr_accessors mk_inherited_accessors mk_output_accessors mk_inherited_merging_accessors mk_accessors /; use HTML::FormFu::Constraint; use HTML::FormFu::Exception; use HTML::FormFu::FakeQuery; use HTML::FormFu::Filter; use HTML::FormFu::Inflator; use HTML::FormFu::Localize; use HTML::FormFu::ObjectUtil qw/ :FORM_AND_BLOCK :FORM_AND_ELEMENT populate load_config_file form clone stash constraints_from_dbic parent get_nested_hash_value set_nested_hash_value nested_hash_key_exists /; use HTML::FormFu::Util qw/ require_class _get_elements xml_escape split_name _parse_args process_attrs _filter_components /; use List::MoreUtils qw/ uniq /; use Scalar::Util qw/ blessed refaddr weaken /; use Storable qw/ dclone /; use Regexp::Copy; use Carp qw/ croak /; use overload 'eq' => sub { refaddr $_[0] eq refaddr $_[1] }, 'ne' => sub { refaddr $_[0] ne refaddr $_[1] }, '==' => sub { refaddr $_[0] eq refaddr $_[1] }, '!=' => sub { refaddr $_[0] ne refaddr $_[1] }, '""' => sub { return shift->render }, bool => sub {1}, fallback => 1; __PACKAGE__->mk_attrs(qw/ attributes /); __PACKAGE__->mk_attr_accessors(qw/ id action enctype method /); __PACKAGE__->mk_accessors( qw/ indicator filename javascript javascript_src query_type languages force_error_message localize_class submitted query input _auto_fieldset _elements _processed_params _valid_names _models _output_processors tt_module params_ignore_underscore nested_name nested_subscript default_model tmp_upload_dir _plugins / ); __PACKAGE__->mk_output_accessors(qw/ form_error_message /); __PACKAGE__->mk_inherited_accessors( qw/ auto_id auto_label auto_error_class auto_error_message auto_constraint_class auto_inflator_class auto_validator_class auto_transformer_class render_method render_processed_value force_errors repeatable_count / ); __PACKAGE__->mk_inherited_merging_accessors(qw/ tt_args config_callback /); *elements = \&element; *constraints = \&constraint; *filters = \&filter; *deflators = \&deflator; *inflators = \&inflator; *validators = \&validator; *transformers = \&transformer; *output_processors = \&output_processor; *loc = \&localize; *plugins = \&plugin; *add_plugins = \&add_plugin; our $VERSION = '0.03002'; $VERSION = eval $VERSION; Class::C3::initialize(); sub new { my $class = shift; my %attrs; eval { %attrs = %{ $_[0] } if @_ }; croak "attributes argument must be a hashref" if $@; my $self = bless {}, $class; my %defaults = ( _elements => [], _output_processors => [], _valid_names => [], _plugins => [], _models => [], _processed_params => {}, input => {}, stash => {}, action => '', method => 'post', filename => 'form', default_args => {}, render_method => 'string', tt_args => {}, tt_module => 'Template', query_type => 'CGI', languages => ['en'], default_model => 'DBIC', localize_class => 'HTML::FormFu::I18N', auto_error_class => 'error_%s_%t', auto_error_message => 'form_%s_%t', ); $self->populate( \%defaults ); $self->populate( \%attrs ); return $self; } sub auto_fieldset { my $self = shift; return $self->_auto_fieldset if !@_; my %opts = ref $_[0] ? %{ $_[0] } : (); $opts{type} = 'Fieldset'; $self->element( \%opts ); $self->_auto_fieldset(1); return $self; } sub default_values { my $self = shift; my %values; eval { %values = %{ $_[0] } }; croak "default_values argument must be a hashref" if $@; for my $field ( @{ $self->get_fields } ) { my $name = $field->nested_name; next unless defined $name; next unless exists $values{$name}; $field->default( $values{$name} ); } return $self; } sub model { my ( $self, $model_name ) = @_; $model_name = $self->default_model if !defined $model_name; for my $model ( @{ $self->_models } ) { return $model if $model->type =~ /\Q$model_name\E$/; } # class not found, try require-ing it my $class = $model_name =~ s/^\+// ? $model_name : "HTML::FormFu::Model::$model_name"; require_class($class); my $model = $class->new( { type => $model_name, parent => $self, } ); push @{ $self->_models }, $model; return $model; } sub model_class { my $self = shift; warn "model_class() method deprecated and is provided for compatibilty only, " . "and will be removed: use default_model instead\n"; return $self->default_model(@_); } sub defaults_from_model { my $self = shift; warn "defaults_from_model() method deprecated and is provided for compatibility only, " . "and will be removed: use \$form->model->default_values() instead\n"; return $self->model->default_values(@_); } sub save_to_model { my $self = shift; warn "save_to_model() method deprecated and is provided for compatibility only, " . "and will be removed: use \$form->model->update() instead\n"; return $self->model->update(@_); } sub process { my $self = shift; $self->input( {} ); $self->_processed_params( {} ); $self->_valid_names( [] ); $self->clear_errors; my $query; if (@_) { $query = shift; $self->query($query); } else { $query = $self->query; } if ( defined $query && !blessed($query) ) { $query = HTML::FormFu::FakeQuery->new( $self, $query ); $self->query($query); } my $plugins = $self->get_plugins; for my $plugin (@$plugins) { $plugin->process; } for my $elem ( @{ $self->get_elements } ) { $elem->process; } my $submitted; if ( defined $query ) { eval { my @params = $query->param }; croak "Invalid query object: $@" if $@; $submitted = $self->_submitted($query); } $self->submitted($submitted); if ($submitted) { my %param; my @params = $query->param; for my $field ( @{ $self->get_fields } ) { my $name = $field->nested_name; next if !defined $name; next if !grep { $name eq $_ } @params; if ( $field->nested ) { my @values = $query->param($name); my $value = @values > 1 ? \@values : $values[0]; $self->set_nested_hash_value( \%param, $name, $value ); } else { my @values = $query->param($name); $param{$name} = @values > 1 ? \@values : $values[0]; } } for my $field ( @{ $self->get_fields } ) { $field->process_input( \%param ); } $self->input( \%param ); $self->_process_input; } # post_process for my $elem ( @{ $self->get_elements } ) { $elem->post_process; } for my $plugin (@$plugins) { $plugin->post_process; } return; } sub _submitted { my ( $self, $query ) = @_; my $indi = $self->indicator; my $code; if ( defined($indi) && ref $indi ne 'CODE' ) { $code = sub { return defined $query->param($indi) }; } elsif ( !defined $indi ) { my @names = uniq( grep {defined} map { $_->nested_name } @{ $self->get_fields } ); $code = sub { grep { defined $query->param($_) } @names; }; } else { $code = $indi; } return $code->( $self, $query ); } sub _process_input { my ($self) = @_; $self->_build_params; $self->_process_file_uploads; $self->_filter_input; $self->_constrain_input; $self->_inflate_input if !@{ $self->get_errors }; $self->_validate_input if !@{ $self->get_errors }; $self->_transform_input if !@{ $self->get_errors }; $self->_build_valid_names; return; } sub _build_params { my ($self) = @_; my $input = $self->input; my %params; for my $field ( @{ $self->get_fields } ) { my $name = $field->nested_name; next if !defined $name; next if exists $params{$name}; next if !$self->nested_hash_key_exists( $self->input, $name ) && !$field->default_empty_value; my $input = $self->get_nested_hash_value( $self->input, $name ); if ( ref $input eq 'ARRAY' ) { # can't clone upload filehandles # so create new arrayref of values $input = [@$input]; } elsif ( !defined $input && $field->default_empty_value ) { $input = ''; } $self->set_nested_hash_value( \%params, $name, $input, $name ); } $self->_processed_params( \%params ); return; } sub _process_file_uploads { my ($self) = @_; my @names = uniq( sort grep {defined} map { $_->nested_name } grep { $_->isa('HTML::FormFu::Element::File') } @{ $self->get_fields } ); if (@names) { my $query_class = $self->query_type; if ( $query_class !~ /^\+/ ) { $query_class = "HTML::FormFu::QueryType::$query_class"; } require_class($query_class); my $params = $self->_processed_params; my $input = $self->input; for my $name (@names) { next if !$self->nested_hash_key_exists( $input, $name ); my $values = $query_class->parse_uploads( $self, $name ); $self->set_nested_hash_value( $params, $name, $values ); } } return; } sub _filter_input { my ($self) = @_; my $params = $self->_processed_params; for my $filter ( @{ $self->get_filters } ) { my $name = $filter->nested_name; next if !defined $name; next if !$self->nested_hash_key_exists( $params, $name ); $filter->process( $self, $params ); } return; } sub _constrain_input { my ($self) = @_; my $params = $self->_processed_params; for my $constraint ( @{ $self->get_constraints } ) { my @errors = eval { $constraint->process($params); }; if ( blessed $@ && $@->isa('HTML::FormFu::Exception::Constraint') ) { push @errors, $@; } elsif ($@) { push @errors, HTML::FormFu::Exception::Constraint->new; } for my $error (@errors) { $error->parent( $constraint->parent ) if !$error->parent; $error->constraint($constraint) if !$error->constraint; $error->parent->add_error($error); } } return; } sub _inflate_input { my ($self) = @_; my $params = $self->_processed_params; for my $inflator ( @{ $self->get_inflators } ) { my $name = $inflator->nested_name; next if !defined $name; next if !$self->nested_hash_key_exists( $params, $name ); next if grep {defined} @{ $inflator->parent->get_errors }; my $value = $self->get_nested_hash_value( $params, $name ); my @errors; ( $value, @errors ) = eval { $inflator->process($value) }; if ( blessed $@ && $@->isa('HTML::FormFu::Exception::Inflator') ) { push @errors, $@; } elsif ($@) { push @errors, HTML::FormFu::Exception::Inflator->new; } for my $error (@errors) { $error->parent( $inflator->parent ) if !$error->parent; $error->inflator($inflator) if !$error->inflator; $error->parent->add_error($error); } $self->set_nested_hash_value( $params, $name, $value ); } return; } sub _validate_input { my ($self) = @_; my $params = $self->_processed_params; for my $validator ( @{ $self->get_validators } ) { my $name = $validator->nested_name; next if !defined $name; next if !$self->nested_hash_key_exists( $params, $name ); next if grep {defined} @{ $validator->parent->get_errors }; my @errors = eval { $validator->process($params) }; if ( blessed $@ && $@->isa('HTML::FormFu::Exception::Validator') ) { push @errors, $@; } elsif ($@) { push @errors, HTML::FormFu::Exception::Validator->new; } for my $error (@errors) { $error->parent( $validator->parent ) if !$error->parent; $error->validator($validator) if !$error->validator; $error->parent->add_error($error); } } return; } sub _transform_input { my ($self) = @_; my $params = $self->_processed_params; for my $transformer ( @{ $self->get_transformers } ) { my $name = $transformer->nested_name; next if !defined $name; next if !$self->nested_hash_key_exists( $params, $name ); next if grep {defined} @{ $transformer->parent->get_errors }; my $value = $self->get_nested_hash_value( $params, $name ); my @errors; ( $value, @errors ) = eval { $transformer->process( $value, $params ) }; if ( blessed $@ && $@->isa('HTML::FormFu::Exception::Transformer') ) { push @errors, $@; } elsif ($@) { push @errors, HTML::FormFu::Exception::Transformer->new; } for my $error (@errors) { $error->parent( $transformer->parent ) if !$error->parent; $error->transformer($transformer) if !$error->transformer; $error->parent->add_error($error); } $self->set_nested_hash_value( $params, $name, $value ); } return; } sub _build_valid_names { my ($self) = @_; my $params = $self->_processed_params; my $skip_private = $self->params_ignore_underscore; my @errors = $self->has_errors; my @names; my %non_param; for my $field ( @{ $self->get_fields } ) { my $name = $field->nested_name; next if !defined $name; next if $skip_private && $field->name =~ /^_/; if ( $field->non_param ) { $non_param{$name} = 1; } elsif ( $self->nested_hash_key_exists( $params, $name ) ) { push @names, $name; } } push @names, grep { ref $params->{$_} ne 'HASH' } grep { !( $skip_private && /^_/ ) } grep { !exists $non_param{$_} } keys %$params; @names = uniq( sort @names ); my %valid; CHECK: for my $name (@names) { for my $error (@errors) { next CHECK if $name eq $error; } $valid{$name}++; } my @valid = keys %valid; $self->_valid_names( \@valid ); return; } sub _hash_keys { my ( $hash, $subscript ) = @_; my @names; for my $key ( keys %$hash ) { if ( ref $hash->{$key} eq 'HASH' ) { push @names, map { $subscript ? "${key}[${_}]" : "$key.$_" } _hash_keys( $hash->{$key}, $subscript ); } elsif ( ref $hash->{$key} eq 'ARRAY' ) { push @names, map { $subscript ? "${key}[${_}]" : "$key.$_" } _array_indices( $hash->{$key}, $subscript ); } else { push @names, $key; } } return @names; } sub _array_indices { my ( $array, $subscript ) = @_; my @names; for my $i ( 0 .. $#{$array} ) { if ( ref $array->[$i] eq 'HASH' ) { push @names, map { $subscript ? "${i}[${_}]" : "$i.$_" } _hash_keys( $array->[$i], $subscript ); } elsif ( ref $array->[$i] eq 'ARRAY' ) { push @names, map { $subscript ? "${i}[${_}]" : "$i.$_" } _array_indices( $array->[$i], $subscript ); } else { push @names, $i; } } return @names; } sub submitted_and_valid { my ($self) = @_; return $self->submitted && !$self->has_errors; } sub params { my ($self) = @_; return {} if !$self->submitted; my @names = $self->valid; my %params; for my $name (@names) { my @values = $self->param($name); if ( @values > 1 ) { $self->set_nested_hash_value( \%params, $name, \@values ); } else { $self->set_nested_hash_value( \%params, $name, $values[0] ); } } return \%params; } sub param { my ( $self, $name ) = @_; croak 'param method is readonly' if @_ > 2; return if !$self->submitted; if ( @_ == 2 ) { return if !$self->valid($name); my $value = $self->get_nested_hash_value( $self->_processed_params, $name ); return if !defined $value; if ( ref $value eq 'ARRAY' ) { return wantarray ? @$value : $value->[0]; } else { return $value; } } # return a list of valid names, if no $name arg return $self->valid; } sub param_value { my ( $self, $name ) = @_; croak 'name parameter required' if @_ != 2; # ignore $form->valid($name) and $form->submitted # this is guaranteed to always return a single value # or undef my $value = $self->get_nested_hash_value( $self->_processed_params, $name ); return ref $value eq 'ARRAY' ? $value->[0] : $value; } sub param_array { my ( $self, $name ) = @_; croak 'name parameter required' if @_ != 2; # guaranteed to always return an arrayref return [] if !$self->valid($name); my $value = $self->get_nested_hash_value( $self->_processed_params, $name ); return [] if !defined $value; return ref $value eq 'ARRAY' ? $value : [$value]; } sub param_list { my ( $self, $name ) = @_; croak 'name parameter required' if @_ != 2; # guaranteed to always return an arrayref return if !$self->valid($name); my $value = $self->get_nested_hash_value( $self->_processed_params, $name ); return if !defined $value; return ref $value eq 'ARRAY' ? @$value : $value; } sub valid { my $self = shift; return if !$self->submitted; my @valid = @{ $self->_valid_names }; if (@_) { my $name = shift; return 1 if grep { $name eq $_ } @valid; # not found - see if it's the name of a nested block my $parent; if ( defined $self->nested_name && $self->nested_name eq $name ) { $parent = $self; } else { ($parent) = grep { $_->isa('HTML::FormFu::Element::Block') } @{ $self->get_all_elements( { nested_name => $name, } ) }; } if ( defined $parent ) { my $fail = grep {defined} map { @{ $_->get_errors } } @{ $parent->get_fields }; return 1 if !$fail; } return; } # return a list of valid names, if no $name arg return @valid; } sub has_errors { my $self = shift; return if !$self->submitted; my @names = map { $_->nested_name } grep { @{ $_->get_errors } } grep { defined $_->nested_name } @{ $self->get_fields }; if (@_) { my $name = shift; return 1 if grep {/\Q$name/} @names; return; } # return list of names with errors, if no $name arg return @names; } sub add_valid { my ( $self, $key, $value ) = @_; croak 'add_valid requires arguments ($key, $value)' unless @_ == 3; $self->set_nested_hash_value( $self->input, $key, $value ); $self->set_nested_hash_value( $self->_processed_params, $key, $value ); push @{ $self->_valid_names }, $key if !grep { $_ eq $key } @{ $self->_valid_names }; return $value; } sub _single_plugin { my ( $self, $arg ) = @_; if ( !ref $arg ) { $arg = { type => $arg }; } elsif ( ref $arg eq 'HASH' ) { $arg = {%$arg}; # shallow clone } else { croak 'invalid args'; } my $type = delete $arg->{type}; my @return; my @names = map { ref $_ ? @$_ : $_ } grep {defined} ( delete $arg->{name}, delete $arg->{names} ); if (@names) { # add plugins to appropriate fields for my $x (@names) { for my $field ( @{ $self->get_fields( { nested_name => $x } ) } ) { my $new = $field->_require_plugin( $type, $arg ); push @{ $field->_plugins }, $new; push @return, $new; } } } else { # add plugin directly to form my $new = $self->_require_plugin( $type, $arg ); push @{ $self->_plugins }, $new; push @return, $new; } return @return; } sub render { my $self = shift; my $plugins = $self->get_plugins; for my $plugin (@$plugins) { $plugin->render; } my $output = $self->next::method(@_); for my $plugin (@$plugins) { $plugin->post_render( \$output ); } return $output; } sub render_data { my $self = shift; my $render = $self->render_data_non_recursive( { elements => [ map { $_->render_data } @{ $self->_elements } ], @_ ? %{ $_[0] } : () } ); return $render; } sub render_data_non_recursive { my $self = shift; my %render = ( filename => $self->filename, javascript => $self->javascript, javascript_src => $self->javascript_src, attributes => xml_escape( $self->attributes ), stash => $self->stash, @_ ? %{ $_[0] } : () ); $render{form} = \%render; weaken( $render{form} ); if ( $self->force_error_message || ( $self->has_errors && defined $self->form_error_message ) ) { $render{form_error_message} = xml_escape( $self->form_error_message ); } return \%render; } sub string { my ( $self, $args ) = @_; $args ||= {}; # start_form template my $render = exists $args->{render_data} ? $args->{render_data} : $self->render_data_non_recursive; my $html = sprintf "", process_attrs( $render->{attributes} ); if ( defined $render->{form_error_message} ) { $html .= sprintf qq{\n
%s
}, $render->{form_error_message}; } if ( defined $render->{javascript_src} ) { my $src = $render->{javascript_src}; $src = [$src] if ref $src ne 'ARRAY'; for my $file (@$src) { $html .= sprintf qq{\n}, $file; } } if ( defined $render->{javascript} ) { $html .= sprintf qq{\n}, $render->{javascript}; } # form template $html .= "\n"; for my $elem ( @{ $self->get_elements } ) { # call render, so that child elements can use a different renderer my $elem_html = $elem->render; # skip Blank fields if ( length $elem_html ) { $html .= $elem_html . "\n"; } } # end_form template $html .= "\n"; return $html; } sub start { my ($self) = @_; return $self->tt( { filename => 'start_form', render_data => $self->render_data_non_recursive, } ); } sub end { my ($self) = @_; return $self->tt( { filename => 'end_form', render_data => $self->render_data_non_recursive, } ); } sub hidden_fields { my ($self) = @_; return join "", map { $_->render } @{ $self->get_fields( { type => 'Hidden' } ) }; } sub output_processor { my ( $self, $arg ) = @_; my @return; if ( ref $arg eq 'ARRAY' ) { push @return, map { $self->_single_output_processor($_) } @$arg; } else { push @return, $self->_single_output_processor($arg); } return @return == 1 ? $return[0] : @return; } sub _single_output_processor { my ( $self, $arg ) = @_; if ( !ref $arg ) { $arg = { type => $arg }; } elsif ( ref $arg eq 'HASH' ) { $arg = dclone($arg); } else { croak 'invalid args'; } my $type = delete $arg->{type}; my $new = $self->_require_output_processor( $type, $arg ); push @{ $self->_output_processors }, $new; return $new; } sub _require_output_processor { my ( $self, $type, $opt ) = @_; croak 'required arguments: $self, $type, \%options' if @_ != 3; eval { my %x = %$opt }; croak "options argument must be hash-ref" if $@; my $class = $type; if ( not $class =~ s/^\+// ) { $class = "HTML::FormFu::OutputProcessor::$class"; } $type =~ s/^\+//; require_class($class); my $object = $class->new( { type => $type, parent => $self, } ); # handle default_args my $parent = $self->parent; if ( $parent && exists $parent->default_args->{output_processor}{$type} ) { %$opt = ( %{ $parent->default_args->{output_processer}{$type} }, %$opt ); } $object->populate($opt); return $object; } sub get_output_processors { my $self = shift; my %args = _parse_args(@_); my @x = @{ $self->_output_processors }; if ( exists $args{type} ) { @x = grep { $_->type eq $args{type} } @x; } return \@x; } sub get_output_processor { my $self = shift; my $x = $self->get_output_processors(@_); return @$x ? $x->[0] : (); } 1; __END__ =head1 NAME HTML::FormFu - HTML Form Creation, Rendering and Validation Framework =head1 BETA SOFTWARE There may be API changes required before the 1.0 release. Any incompatible changes will first be discussed on the L. See L for further details. Work is still needed on the documentation, if you come across any errors or find something confusing, please give feedback via the L. =head1 SYNOPSIS Note: These examples make use of L. As of C v02.005, the L module is not bundled with C and is available in a stand-alone distribution. use HTML::FormFu; my $form = HTML::FormFu->new; $form->load_config_file('form.yml'); $form->process( $cgi_query ); if ( $form->submitted_and_valid ) { # do something with $form->params } else { # display the form $template->param( form => $form ); } If you're using L, a more suitable example might be: package MyApp::Controller::User; use strict; use base 'Catalyst::Controller::HTML::FormFu'; sub user : Chained CaptureArgs(1) { my ( $self, $c, $id ) = @_; my $rs = $c->model('Schema')->resultset('User'); $c->stash->{user} = $rs->find( $id ); return; } sub edit : Chained('user') Args(0) FormConfig { my ( $self, $c ) = @_; my $form = $c->stash->{form}; my $user = $c->stash->{user}; if ( $form->submitted_and_valid ) { $form->model->update( $user ); $c->res->redirect( $c->uri_for( "/user/$id" ) ); return; } $form->model->default_values( $user ) if ! $form->submitted; } Here's an example of a config file to create a basic login form (all examples here are L, but you can use any format supported by L), you can also create forms directly in your perl code, rather than using an external config file. --- action: /login indicator: submit auto_fieldset: 1 elements: - type: Text name: user constraints: - Required - type: Password name: pass constraints: - Required - type: Submit name: submit constraints: - SingleValue =head1 DESCRIPTION L is a HTML form framework which aims to be as easy as possible to use for basic web forms, but with the power and flexibility to do anything else you might want to do (as long as it involves forms). You can configure almost any part of formfu's behaviour and output. By default formfu renders "XHTML 1.0 Strict" compliant markup, with as little extra markup as possible, but with sufficient CSS class names to allow for a wide-range of output styles to be generated by changing only the CSS. All methods listed below (except L) can either be called as a normal method on your C<$form> object, or as an option in your config file. Examples will mainly be shown in L config syntax. This documentation follows the convention that method arguments surrounded by square brackets C<[]> are I, and all other arguments are required. =head1 BUILDING A FORM =head2 new Arguments: [\%options] Return Value: $form Create a new L object. Any method which can be called on the L object may instead be passed as an argument to L. my $form = HTML::FormFu->new({ action => '/search', method => 'GET', auto_fieldset => 1, }); =head2 load_config_file Arguments: $filename Arguments: \@filenames Return Value: $form Accepts a filename or list of file names, whose filetypes should be of any format recognized by L. The content of each config file is passed to L, and so are added to the form. L may be called in a config file itself, so as to allow common settings to be kept in a single config file which may be loaded by any form. --- load_config_file: - file1 - file2 YAML multiple documents within a single file. The document start marker is a line containing 3 dashes. Multiple documents will be applied in order, just as if multiple filenames had been given. In the following example, multiple documents are taken advantage of to load another config file after the elements are added. (If this were a single document, the C would be called before C, regardless of it's position in the file). --- elements: - name: one - name: two --- load_config_file: ext.yml Like perl's C function, relative-paths are resolved from the current working directory. If you're using the C action controller in L, see L. See L for advice on organising config files. =head2 config_callback Arguments: \%options If defined, the arguments are used to create a L object during L which may be used to pre-process the config before it is sent to L. For example, the code below adds a callback to a form that will dynamically alter any config value ending in ".yml" to end in ".yaml" when you call L: $form->config_callback({ plain_value => sub { my( $visitor, $data ) = @_; s/\.yml/.yaml/; } }); Default Value: not defined This method is a special 'inherited accessor', which means it can be set on the form, a block element or a single element. When the value is read, if no value is defined it automatically traverses the element's hierarchy of parents, through any block elements and up to the form, searching for a defined value. =head2 populate Arguments: \%options Return Value: $form Each option key/value passed may be any L method-name and arguments. Provides a simple way to set multiple values, or add multiple elements to a form with a single method-call. Attempts to call the method-names in a semi-intelligent order (see the source of populate() in C for details). =head2 default_values Arguments: \%defaults Return Value: $form Set multiple field's default values from a single hash-ref. The hash-ref's keys correspond to a form field's name, and the value is passed to the field's L. This should be called after all fields have been added to the form, and before L is called (otherwise, call L again before rendering the form). =head2 indicator Arguments: $field_name Arguments: \&coderef If L is set to a fieldname, L will return true if a value for that fieldname was submitted. If L is set to a code-ref, it will be called as a subroutine with the two arguments C<$form> and C<$query>, and its return value will be used as the return value for L. If L is not set, L will return true if a value for any known fieldname was submitted. =head2 auto_fieldset Arguments: 1 Arguments: \%options Return Value: $fieldset This setting is suitable for most basic forms, and means you can generally ignore adding fieldsets yourself. Calling C<< $form->auto_fieldset(1) >> immediately adds a fieldset element to the form. Thereafter, C<< $form->elements() >> will add all elements (except fieldsets) to that fieldset, rather than directly to the form. To be specific, the elements are added to the L fieldset on the form, so if you add another fieldset, any further elements will be added to that fieldset. Also, you may pass a hashref to auto_fieldset(), and this will be used to set defaults for the first fieldset created. A few examples and their output, to demonstrate: 2 elements with no fieldset. --- elements: - type: Text name: foo - type: Text name: bar
2 elements with an L. --- auto_fieldset: 1 elements: - type: Text name: foo - type: Text name: bar
The 3rd element is within a new fieldset --- auto_fieldset: { id: fs } elements: - type: Text name: foo - type: Text name: bar - type: Fieldset - type: Text name: baz
Because of this behaviour, if you want nested fieldsets you will have to add each nested fieldset directly to its intended parent. my $parent = $form->get_element({ type => 'Fieldset' }); $parent->element('fieldset'); =head2 form_error_message Arguments: $string Normally, input errors cause an error message to be displayed alongside the appropriate form field. If you'd also like a general error message to be displayed at the top of the form, you can set the message with L. To change the markup used to display the message, edit the C template file. =head2 form_error_message_xml Arguments: $string If you don't want your error message to be XML-escaped, use the L method instead. =head2 form_error_message_loc Arguments: $localization_key For ease of use, if you'd like to use the provided localized error message, set L to the value C. You can, of course, set L to any key in your I18N file. =head2 force_error_message If true, forces the L to be displayed even if there are no field errors. =head2 default_args Arguments: \%defaults Set defaults which will be added to every element, constraint, etc. of the listed type which is added to the form. For example, to make every C element automatically have a L of C<10>, and make every C deflator automatically get it's strftime set to C<%d/%m/%Y>: element_args: elements: Text: size: 10 deflators: Strftime: strftime: '%d/%m/%Y' To take it even further, you can even make all DateTime elements automatically get an appropriate Strftime deflator and a DateTime inflator: element_args: elements: DateTime: deflators: type: Strftime strftime: '%d-%m-%Y' inflators: type: DateTime parser: strptime: '%d-%m-%Y' Note: Unlike the proper methods which have aliases, for example L which is an alias for L - the keys given to C must be of the plural form, e.g.: element_args: elements: {} deflators: {} filters: {} constraints: {} inflators: {} validators: {} transformers: {} output_processors: {} =head2 javascript Arguments: [$javascript] If set, the contents will be rendered within a C