package HTML::FormFu::Role::Element::Field;
use Moose::Role;
use MooseX::Aliases;
with 'HTML::FormFu::Role::ContainsElementsSharedWithField',
'HTML::FormFu::Role::NestedHashUtils';
use HTML::FormFu::Attribute qw(
mk_attrs
mk_output_accessors
mk_inherited_accessors
);
use HTML::FormFu::Constants qw( $EMPTY_STR );
use HTML::FormFu::Util qw(
_parse_args append_xml_attribute
xml_escape require_class
process_attrs _filter_components
);
use Class::MOP::Method;
use Clone ();
use List::MoreUtils qw( uniq );
use Carp qw( croak );
__PACKAGE__->mk_attrs( qw(
comment_attributes
container_attributes
label_attributes
) );
has _constraints => ( is => 'rw', traits => ['Chained'] );
has _filters => ( is => 'rw', traits => ['Chained'] );
has _inflators => ( is => 'rw', traits => ['Chained'] );
has _deflators => ( is => 'rw', traits => ['Chained'] );
has _validators => ( is => 'rw', traits => ['Chained'] );
has _transformers => ( is => 'rw', traits => ['Chained'] );
has _plugins => ( is => 'rw', traits => ['Chained'] );
has _errors => ( is => 'rw', traits => ['Chained'] );
has container_tag => ( is => 'rw', traits => ['Chained'] );
has field_filename => ( is => 'rw', traits => ['Chained'] );
has label_filename => ( is => 'rw', traits => ['Chained'] );
has label_tag => ( is => 'rw', traits => ['Chained'] );
has retain_default => ( is => 'rw', traits => ['Chained'] );
has force_default => ( is => 'rw', traits => ['Chained'] );
has javascript => ( is => 'rw', traits => ['Chained'] );
has non_param => ( is => 'rw', traits => ['Chained'] );
has reverse_single => ( is => 'rw', traits => ['Chained'] );
has reverse_multi => ( is => 'rw', traits => ['Chained'] );
has multi_value => ( is => 'rw', traits => ['Chained'] );
has original_name => ( is => 'rw', traits => ['Chained'] );
has original_nested_name => ( is => 'rw', traits => ['Chained'] );
__PACKAGE__->mk_output_accessors(qw( comment label value ));
__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_processed_value force_errors
repeatable_count default_empty_value
locale
) );
alias( "default", "value" );
alias( "default_xml", "value_xml" );
alias( "default_loc", "value_loc" );
after BUILD => sub {
my $self = shift;
$self->_constraints( [] );
$self->_filters( [] );
$self->_deflators( [] );
$self->_inflators( [] );
$self->_validators( [] );
$self->_transformers( [] );
$self->_plugins( [] );
$self->_errors( [] );
$self->comment_attributes( {} );
$self->container_attributes( {} );
$self->label_attributes( {} );
$self->label_filename('label');
$self->label_tag('label');
$self->container_tag('div');
$self->is_field(1);
return;
};
sub nested {
my ($self) = @_;
croak 'cannot set nested' if @_ > 1;
if ( defined $self->name ) {
my $parent = $self;
while ( defined( $parent = $parent->parent ) ) {
if ( $parent->can('is_field') && $parent->is_field ) {
return 1 if defined $parent->name;
}
else {
return 1 if defined $parent->nested_name;
}
}
}
return;
}
sub nested_name {
my ($self) = @_;
croak 'cannot set nested_name' if @_ > 1;
return if !defined $self->name;
my @names = $self->nested_names;
if ( $self->form->nested_subscript ) {
my $name = shift @names;
map { $name .= "[$_]" } @names;
# TODO - Mario Minati 19.05.2009
# Does this (name formatted as '[name]') collide with FF::Model::HashRef as
# it uses /_\d/ to parse repeatable names?
return $name;
}
else {
return join ".", @names;
}
}
sub nested_names {
my ($self) = @_;
croak 'cannot set nested_names' if @_ > 1;
if ( defined( my $name = $self->name ) ) {
my @names;
my $parent = $self;
# micro optimization! this method's called a lot, so access
# parent hashkey directly, instead of calling parent()
while ( defined( $parent = $parent->{parent} ) ) {
if ( $parent->can('is_field') && $parent->is_field ) {
# handling Field
push @names, $parent->name
if defined $parent->name;
}
elsif ( $parent->can('is_repeatable') && $parent->is_repeatable ) {
# handling Repeatable
# ignore Repeatables nested_name attribute as it is provided
# by the childrens Block elements
}
else {
# handling 'not Field' and 'not Repeatable'
push @names, $parent->nested_name
if defined $parent->nested_name;
}
}
if (@names) {
return reverse $name, @names;
}
}
return ( $self->name );
}
sub build_original_nested_name {
my ($self) = @_;
croak 'cannot set build_original_nested_name' if @_ > 1;
return if !defined $self->name;
my @names = $self->build_original_nested_names;
if ( $self->form->nested_subscript ) {
my $name = shift @names;
map { $name .= "[$_]" } @names;
# TODO - Mario Minati 19.05.2009
# Does this (name formatted as '[name]') collide with FF::Model::HashRef as
# it uses /_\d/ to parse repeatable names?
return $name;
}
else {
return join ".", @names;
}
}
sub build_original_nested_names {
my ($self) = @_;
croak 'cannot set build_original_nested_names' if @_ > 1;
# TODO - Mario Minati 19.05.2009
# Maybe we have to use original_name instead of name.
# Yet there is no testcase, which is currently failing.
if ( defined( my $name = $self->name ) ) {
my @names;
my $parent = $self;
# micro optimization! this method's called a lot, so access
# parent hashkey directly, instead of calling parent()
while ( defined( $parent = $parent->{parent} ) ) {
if ( $parent->can('is_field') && $parent->is_field ) {
# handling Field
if (defined $parent->original_name) {
push @names, $parent->original_name;
}
elsif (defined $parent->name) {
push @names, $parent->name;
}
}
elsif ( $parent->can('is_repeatable') && $parent->is_repeatable ) {
# handling Repeatable
# TODO - Mario Minati 19.05.2009
# Do we have to take care of chains of Repeatable elements, if the Block
# elements have already been created for the outer Repeatable elements to
# avoid 'outer.outer_1.inner'
# Yet there is no failing testcase. All testcases in FF and FF::Model::DBIC
# which have nested repeatable elements are passing currently.
push @names, $parent->original_nested_name
if defined $parent->original_nested_name;
}
else {
# handling 'not Field' and 'not Repeatable'
if ($parent->can('original_nested_name') && defined $parent->original_nested_name) {
push @names, $parent->original_nested_name;
}
elsif (defined $parent->nested_name) {
push @names, $parent->nested_name
}
}
}
if (@names) {
return reverse $name, @names;
}
}
return ( $self->name );
}
sub nested_base {
my ($self) = @_;
croak 'cannot set nested_base' if @_ > 1;
my $parent = $self;
while ( defined( $parent = $parent->parent ) ) {
return $parent->nested_name if defined $parent->nested_name;
}
}
sub get_deflators {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_deflators };
return _filter_components( \%args, \@x );
}
sub get_filters {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_filters };
return _filter_components( \%args, \@x );
}
sub get_constraints {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_constraints };
return _filter_components( \%args, \@x );
}
sub get_inflators {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_inflators };
return _filter_components( \%args, \@x );
}
sub get_validators {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_validators };
return _filter_components( \%args, \@x );
}
sub get_transformers {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_transformers };
return _filter_components( \%args, \@x );
}
sub get_errors {
my $self = shift;
my %args = _parse_args(@_);
my @x = @{ $self->_errors };
_filter_components( \%args, \@x );
if ( !$args{forced} ) {
@x = grep { !$_->forced } @x;
}
return \@x;
}
sub clear_errors {
my ($self) = @_;
$self->_errors( [] );
return;
}
after pre_process => sub {
my $self = shift;
for my $plugin ( @{ $self->_plugins } ) {
$plugin->pre_process;
}
return;
};
after process => sub {
my $self = shift;
for my $plugin ( @{ $self->_plugins } ) {
$plugin->process;
}
return;
};
after post_process => sub {
my $self = shift;
for my $plugin ( @{ $self->_plugins } ) {
$plugin->post_process;
}
return;
};
sub process_input {
my ( $self, $input ) = @_;
my $submitted = $self->form->submitted;
my $default = $self->default;
my $original = $self->value;
my $name = $self->nested_name;
# set input to default value (defined before calling FormFu->process)
if ( $submitted && $self->force_default && defined $default ) {
$self->set_nested_hash_value( $input, $name, $default );
}
# checkbox, radio
elsif ( $submitted && $self->force_default && $self->checked ) {
# the checked attribute is set, so force input to be the original value
$self->set_nested_hash_value( $input, $name, $original );
}
# checkbox, radio
elsif ($submitted
&& $self->force_default
&& !defined $default
&& defined $original )
{
# default and value are not equal, so this element is not checked by default
$self->set_nested_hash_value( $input, $name, undef );
}
return;
}
sub prepare_id {
my ( $self, $render ) = @_;
if ( !defined $render->{attributes}{id}
&& defined $self->auto_id
&& length $self->auto_id )
{
my $form_name
= defined $self->form->id
? $self->form->id
: $EMPTY_STR;
my $field_name
= defined $render->{nested_name}
? $render->{nested_name}
: $EMPTY_STR;
my %string = (
f => $form_name,
n => $field_name,
);
my $id = $self->auto_id;
$id =~ s/%([fn])/$string{$1}/g;
if ( defined( my $count = $self->repeatable_count ) ) {
$id =~ s/%r/$count/g;
}
$render->{attributes}{id} = $id;
}
return;
}
sub process_value {
my ( $self, $value ) = @_;
my $submitted = $self->form->submitted;
my $default = $self->default;
my $new;
if ($submitted) {
if ( defined $value ) {
$new = $value;
}
elsif ( defined $default ) {
$new = $EMPTY_STR;
}
}
else {
$new = $default;
}
if ( $submitted
&& $self->retain_default
&& defined $new
&& $new eq $EMPTY_STR )
{
$new = $default;
}
# if the default value has been changed after FormFu->process has been
# called we use it and set the value to that changed default again
if ( $submitted
&& $self->force_default
&& defined $default
&& $new ne $default )
{
$new = $default;
}
return $new;
}
around render_data_non_recursive => sub {
my ( $orig, $self, $args ) = @_;
my $render = $self->$orig({
nested_name => xml_escape( $self->nested_name ),
comment_attributes => xml_escape( $self->comment_attributes ),
container_attributes => xml_escape( $self->container_attributes ),
label_attributes => xml_escape( $self->label_attributes ),
comment => xml_escape( $self->comment ),
label => xml_escape( $self->label ),
field_filename => $self->field_filename,
label_filename => $self->label_filename,
label_tag => $self->label_tag,
container_tag => $self->container_tag,
reverse_single => $self->reverse_single,
reverse_multi => $self->reverse_multi,
javascript => $self->javascript,
$args ? %$args : (),
});
$self->_render_container_class($render);
$self->_render_comment_class($render);
$self->_render_label($render);
$self->_render_value($render);
$self->_render_constraint_class($render);
$self->_render_inflator_class($render);
$self->_render_validator_class($render);
$self->_render_transformer_class($render);
$self->_render_error_class($render);
return $render;
};
sub _render_label {
my ( $self, $render ) = @_;
if ( !defined $render->{label}
&& defined $self->auto_label
&& length $self->auto_label )
{
my %string = (
f => defined $self->form->id ? $self->form->id : '',
n => defined $render->{name} ? $render->{name} : '',
);
my $label = $self->auto_label;
$label =~ s/%([fn])/$string{$1}/g;
$render->{label} = $self->form->localize($label);
}
if ( defined $render->{label} ) {
append_xml_attribute( $render->{container_attributes},
'class', $self->label_tag );
}
# label "for" attribute
if ( defined $render->{label}
&& defined $render->{attributes}{id}
&& !exists $render->{label_attributes}{for} )
{
$render->{label_attributes}{for} = $render->{attributes}{id};
}
return;
}
sub _render_comment_class {
my ( $self, $render ) = @_;
if ( defined $render->{comment} ) {
append_xml_attribute( $render->{comment_attributes},
'class', 'comment' );
append_xml_attribute( $render->{container_attributes},
'class', 'comment' );
}
return;
}
sub _render_value {
my ( $self, $render ) = @_;
my $form = $self->form;
my $name = $self->nested_name;
my $input;
if ( $self->form->submitted
&& defined $name
&& $self->nested_hash_key_exists( $form->input, $name ) )
{
if ( $self->render_processed_value ) {
$input
= $self->get_nested_hash_value( $form->_processed_params, $name,
);
}
else {
$input = $self->get_nested_hash_value( $form->input, $name, );
}
}
if ( ref $input eq 'ARRAY' ) {
my $elems = $self->form->get_fields( $self->name );
for ( 0 .. @$elems - 1 ) {
if ( $self == $elems->[$_] ) {
$input = $input->[$_];
}
}
}
my $value = $self->process_value($input);
if ( !$self->form->submitted
|| ( $self->render_processed_value && defined $value ) )
{
for my $deflator ( @{ $self->_deflators } ) {
$value = $deflator->process($value);
}
}
# handle multiple values for the same name
if ( ref $value eq 'ARRAY' && defined $self->name ) {
my $max = $#$value;
my $fields = $self->form->get_fields( name => $self->name );
for my $i ( 0 .. $max ) {
if ( defined $fields->[$i] && $fields->[$i] eq $self ) {
$value = $value->[$i];
last;
}
}
}
$render->{value} = xml_escape($value);
return;
}
sub _render_container_class {
my ( $self, $render ) = @_;
my $type = $self->type;
$type =~ s/:://g;
append_xml_attribute( $render->{container_attributes}, 'class', lc($type),
);
return;
}
sub _render_constraint_class {
my ( $self, $render ) = @_;
my $auto_class = $self->auto_constraint_class;
return if !defined $auto_class;
for my $c ( @{ $self->_constraints } ) {
my %string = (
f => defined $self->form->id ? $self->form->id : '',
n => defined $render->{name} ? $render->{name} : '',
t => defined $c->type ? lc( $c->type ) : '',
);
$string{t} =~ s/::/_/g;
$string{t} =~ s/\+//;
my $class = $auto_class;
$class =~ s/%([fnt])/$string{$1}/g;
append_xml_attribute( $render->{container_attributes},
'class', $class, );
}
return;
}
sub _render_inflator_class {
my ( $self, $render ) = @_;
my $auto_class = $self->auto_inflator_class;
return if !defined $auto_class;
for my $c ( @{ $self->_inflators } ) {
my %string = (
f => defined $self->form->id ? $self->form->id : '',
n => defined $render->{name} ? $render->{name} : '',
t => defined $c->type ? lc( $c->type ) : '',
);
$string{t} =~ s/::/_/g;
$string{t} =~ s/\+//;
my $class = $auto_class;
$class =~ s/%([fnt])/$string{$1}/g;
append_xml_attribute( $render->{container_attributes},
'class', $class, );
}
return;
}
sub _render_validator_class {
my ( $self, $render ) = @_;
my $auto_class = $self->auto_validator_class;
return if !defined $auto_class;
for my $c ( @{ $self->_validators } ) {
my %string = (
f => defined $self->form->id ? $self->form->id : '',
n => defined $render->{name} ? $render->{name} : '',
t => defined $c->type ? lc( $c->type ) : '',
);
$string{t} =~ s/::/_/g;
$string{t} =~ s/\+//;
my $class = $auto_class;
$class =~ s/%([fnt])/$string{$1}/g;
append_xml_attribute( $render->{container_attributes},
'class', $class, );
}
return;
}
sub _render_transformer_class {
my ( $self, $render ) = @_;
my $auto_class = $self->auto_transformer_class;
return if !defined $auto_class;
for my $c ( @{ $self->_transformers } ) {
my %string = (
f => defined $self->form->id ? $self->form->id : '',
n => defined $render->{name} ? $render->{name} : '',
t => defined $c->type ? lc( $c->type ) : '',
);
$string{t} =~ s/::/_/g;
$string{t} =~ s/\+//;
my $class = $auto_class;
$class =~ s/%([fnt])/$string{$1}/g;
append_xml_attribute( $render->{container_attributes},
'class', $class, );
}
return;
}
sub _render_error_class {
my ( $self, $render ) = @_;
my @errors = @{ $self->get_errors( { forced => 1 } ) };
if (@errors) {
$render->{errors} = \@errors;
append_xml_attribute( $render->{container_attributes},
'class', 'error' );
my @class = uniq map { $_->class } @errors;
for my $class (@class) {
append_xml_attribute( $render->{container_attributes},
'class', $class, );
}
}
return;
}
sub render_label {
my ($self) = @_;
return $self->tt( { filename => $self->{label_filename} } );
}
sub render_field {
my ($self) = @_;
return $self->tt( { filename => $self->{field_filename} } );
}
sub _string_field_start {
my ( $self, $render ) = @_;
# field wrapper template - start
my $html = '';
if ( defined $render->{container_tag} ) {
$html .= sprintf '<%s%s>',
$render->{container_tag},
process_attrs( $render->{container_attributes} );
}
if ( defined $render->{label} && $render->{label_tag} eq 'legend' ) {
$html .= sprintf "\n%s", $self->_string_label($render);
}
if ( $render->{errors} ) {
for my $error ( @{ $render->{errors} } ) {
$html .= sprintf qq{\n%s},
$error->class,
$error->message,
;
}
}
if ( defined $render->{label} && $render->{label_tag} ne 'legend' &&
!$render->{reverse_single}) {
$html .= sprintf "\n%s", $self->_string_label($render);
}
if ( defined $render->{container_tag} ) {
$html .= "\n";
}
return $html;
}
sub _string_label {
my ( $self, $render ) = @_;
# label template
my $html = sprintf "<%s%s>%s%s>",
$render->{label_tag},
process_attrs( $render->{label_attributes} ),
$render->{label},
$render->{label_tag},
;
return $html;
}
sub _string_field_end {
my ( $self, $render ) = @_;
# field wrapper template - end
my $html = '';
if ( defined $render->{label} && $render->{label_tag} ne 'legend' &&
$render->{reverse_single} )
{
$html .= sprintf "\n%s", $self->_string_label($render);
}
if ( defined $render->{comment} ) {
$html .= sprintf "\n\n%s\n",
process_attrs( $render->{comment_attributes} ),
$render->{comment},
;
}
if ( defined $render->{container_tag} ) {
$html .= sprintf "\n%s>", $render->{container_tag},;
}
if ( defined $render->{javascript} ) {
$html .= sprintf qq{\n},
$render->{javascript},
;
}
return $html;
}
around clone => sub {
my $orig = shift;
my $self = shift;
my $clone = $self->$orig(@_);
for my $list ( qw(
_filters _constraints _inflators _validators _transformers
_deflators _errors _plugins )
)
{
$clone->$list( [ map { $_->clone } @{ $self->$list } ] );
map { $_->parent($clone) } @{ $clone->$list };
}
$clone->comment_attributes( Clone::clone( $self->comment_attributes ) );
$clone->container_attributes( Clone::clone( $self->container_attributes ) );
$clone->label_attributes( Clone::clone( $self->label_attributes ) );
return $clone;
};
1;
__END__
=head1 NAME
HTML::FormFu::Element::_Field - base class for all form-field elements
=head1 DESCRIPTION
Base-class for all form-field elements.
=head1 METHODS
=head2 default
Set the form-field's default value.
=head2 default_xml
Arguments: $string
If you don't want the default value to be XML-escaped, use the
L method instead of L.
=head2 default_loc
Arguments: $localization_key
Set the default value using a L10N key.
=head2 value
For most fields, L is an alias for L.
For the L and
L elements, L sets what the value of
the field will be if it is checked or selected. If the L is the
same as the L, then the field will be checked or selected when
rendered.
For the L and
L elements, the L is ignored:
L or
L provides the equivalent
function.
=head2 value_xml
Arguments: $string
If you don't want the value to be XML-escaped, use the L
method instead of L.
=head2 value_loc
Arguments: $localization_key
Set the value using a L10N key.
=head2 non_param
Arguments: bool
If true, values for this field are never returned by L,
L and L.
This is useful for Submit buttons, when you only use its value as an
L.
Default Value: false
=head2 label
Set a label to communicate the purpose of the form-field to the user.
=head2 label_xml
Arguments: $string
If you don't want the label to be XML-escaped, use the L
method instead of L.
=head2 label_loc
Arguments: $localization_key
Set the label using a L10N key.
=head2 comment
Set a comment to be displayed along with the form-field.
=head2 comment_xml
Arguments: $string
If you don't want the comment to be XML-escaped, use the L
method instead of L.
=head2 comment_loc
Arguments: $localization_key
Set the comment using a L10N key.
=head2 container_tag
Set which tag-name should be used to contain the various field parts (field,
label, comment, errors, etc.).
Default Value: 'div'
=head2 javascript
Arguments: [$javascript]
If set, the contents will be rendered within a C