package Rose::HTML::Form::Field; use strict; use Carp(); use Scalar::Util(); use Rose::HTML::Util(); use Rose::HTML::Label; use Rose::HTML::Object::Errors qw(:field); use Rose::HTML::Object::Messages qw(:field); use Rose::HTML::Object; our @ISA = qw(Rose::HTML::Object); use constant HTML_ERROR_SEP => "
\n"; use constant XHTML_ERROR_SEP => "
\n"; use Rose::HTML::Form::Constants qw(FF_SEPARATOR); our $VERSION = '0.554'; #our $Debug = 0; use Rose::HTML::Object::MakeMethods::Localization ( localized_message => [ qw(_error_label label description) ], ); use Rose::Object::MakeMethods::Generic ( scalar => [ qw(rank type) ], boolean => [ qw(required is_cleared has_partial_value) ], boolean => [ trim_spaces => { default => 1 }, empty_is_ok => { default => 0 }, ], 'scalar --get_set_init' => [ qw(html_prefix html_suffix html_error_separator xhtml_error_separator local_moniker apply_error_class) ], ); __PACKAGE__->add_valid_html_attrs(qw( name value onblur onfocus accesskey tabindex )); sub is_button { 0 } *label_id = \&label_message_id; sub error_label { my($self) = shift; if(@_) { return $self->_error_label(@_); } my $label = $self->_error_label; no warnings 'uninitialized'; return (defined $label) ? (length $label ? $label : undef) : $self->label; } sub error_label_id { my($self) = shift; if(@_) { return $self->_error_label_message_id(@_); } my $label = $self->_error_label_message_id; no warnings 'uninitialized'; return (defined $label) ? (length $label ? $label : undef) : $self->label; } sub init_apply_error_class { 1 } sub auto_invalidate_parent { my($self) = shift; if(@_) { return $self->{'auto_invalidate_parent'} = $_[0] ? 1 : 0; } return defined($self->{'auto_invalidate_parent'}) ? $self->{'auto_invalidate_parent'} : ($self->{'auto_invalidate_parent'} = 1); } sub invalidate_value { $_[0]->{'input_value'} = undef; $_[0]->{'internal_value'} = undef; $_[0]->{'output_value'} = undef; } sub invalidate_output_value { $_[0]->{'output_value'} = undef; } sub parent_field { my($self) = shift; if(@_) { if(ref $_[0]) { Scalar::Util::weaken($self->{'parent_field'} = shift); return $self->{'parent_field'}; } else { return $self->{'parent_field'} = shift; } } return $self->{'parent_field'}; } sub parent_form { my($self) = shift; if(@_) { if(ref $_[0]) { Scalar::Util::weaken($self->{'parent_form'} = shift); return $self->{'parent_form'}; } else { return $self->{'parent_form'} = shift; } } return $self->{'parent_form'}; } sub field_depth { my($self) = shift; my $parent = $self->parent_field || $self->parent_form; return 0 unless($parent); my $depth = 1; $depth++ while($parent = $parent->parent_field || $parent->parent_form); return $depth; } sub fq_name { my($self) = shift; return join(FF_SEPARATOR, grep { defined } $self->form_context_name, $self->field_context_name, $self->local_name); } sub fq_moniker { my($self) = shift; return join(FF_SEPARATOR, grep { defined } $self->form_context_name, $self->field_context_name, $self->local_moniker); } sub init_local_moniker { shift->local_name } sub form_context_name { my($self) = shift; my $parent_form = $self->parent_form or return; return $parent_form->fq_form_name or return; } sub field_context_name { my($self) = shift; my $parent_field = $self->parent_field or return; return $parent_field->fq_name or return; } sub is_flat_group { 0 } sub init_html_prefix { '' } sub init_html_suffix { '' } sub init_html_error_separator { HTML_ERROR_SEP } sub init_xhtml_error_separator { XHTML_ERROR_SEP } sub value { my($self) = shift; if(@_) { return $self->input_value($self->html_attr('value', shift)); } else { return $self->html_attr('value'); } } sub resync_name { my($self) = shift; $self->html_attr('name', undef); $self->name if($self->parent_field || $self->parent_form); #$self->name($self->fq_name); } sub local_name { my($self) = shift; if(@_) { my $name = shift; no warnings 'uninitialized'; if(index($name, FF_SEPARATOR) >= 0 && !$self->isa('Rose::HTML::Form::Field::Hidden')) { Carp::croak "Invalid local field name: $name"; } my $old_name = $self->{'local_name'}; $self->{'local_name'} = $name; if(defined $old_name && $name ne $old_name) { if(my $parent_form = $self->parent_form) { $parent_form->delete_field($old_name); $parent_form->add_field($name => $self); } if(my $parent_field = $self->parent_field) { $parent_field->delete_field($old_name); $parent_field->add_field($name => $self); } } return $name; } my $name = $self->{'local_name'}; return $name if(defined $name); return $self->{'local_name'} = $self->{'local_moniker'}; } sub name { my($self) = shift; if(@_) { $self->local_name(shift); return $self->html_attr('name', $self->fq_name); } my $name = $self->html_attr('name'); # The name HTML attr will be an empty string if it's a required attr, # so use length() and not defined() no warnings 'uninitialized'; unless(length $name) { return $self->html_attr('name', $self->fq_name); } return $name; } sub moniker { my($self) = shift; if(@_) { return $self->fq_moniker($self->{'moniker'} = shift); } else { return $self->{'moniker'} if(defined $self->{'moniker'}); return $self->{'moniker'} = $self->fq_moniker; } } sub default_value { my($self) = shift; if(@_) { $self->{'internal_value'} = undef; $self->{'output_value'} = undef; return $self->{'default_value'} = shift; } return $self->{'default_value'}; } sub default { shift->default_value(@_) } sub inflate_value { $_[1] } sub deflate_value { $_[1] } sub input_value { my($self) = shift; if(@_) { $self->{'is_cleared'} = 0; $self->{'internal_value'} = undef; $self->{'output_value'} = undef; $self->{'errors'} = undef; $self->{'input_value'} = shift; if(my $parent = $self->parent_field) { $parent->is_cleared(0) if(!$parent->{'in_init'} && $parent->_is_full); if($self->auto_invalidate_parent) { $parent->invalidate_value; } } return $self->{'input_value'}; } return undef if($self->is_cleared || $self->has_partial_value); my $value = (defined $self->{'input_value'}) ? $self->{'input_value'} : $self->default_value; if(wantarray && ref $value eq 'ARRAY') { return @$value; } return $value; } sub _set_input_value { # XXX: Evil, but I can't bear to add 3 method calls to # XXX: save and then restore this value. local $_[0]->{'auto_invalidate_parent'} = 0; shift->input_value(@_); } sub _is_full { my($self) = shift; if($self->is_full(@_)) { $self->has_partial_value(0); return 1; } return 0; } sub input_value_filtered { my($self) = shift; my $value = $self->input_value; $value = $self->input_prefilter($value); if(my $input_filter = $self->input_filter) { local $_ = $value; $value = $input_filter->($self, $value); } return $value; } sub internal_value { my($self) = shift; Carp::croak "Cannot set the internal value. Use input_value() instead." if(@_); return undef if($self->is_cleared || $self->has_partial_value); if(defined $self->{'internal_value'}) { if(wantarray && ref $self->{'internal_value'} eq 'ARRAY') { return @{$self->{'internal_value'}}; } return $self->{'internal_value'}; } my $value = $self->input_value; my($using_default, $final_value); unless(defined $value) { $value = $self->default_value; $using_default++; } $value = $self->input_prefilter($value); if(my $input_filter = $self->input_filter) { local $_ = $value; $final_value = $input_filter->($self, $value); } else { $final_value = $value } $final_value = $self->inflate_value($final_value); $self->{'internal_value'} = $final_value unless($using_default); if(wantarray && ref $final_value eq 'ARRAY') { return @$final_value; } return $final_value; } sub output_value { my($self) = shift; Carp::croak "Cannot set the output value. Use input_value() instead." if(@_); return undef if($self->is_cleared); return $self->{'output_value'} if(defined $self->{'output_value'}); my $value = $self->deflate_value(scalar $self->internal_value); if(my $output_filter = $self->output_filter) { local $_ = $value; $self->{'output_value'} = $output_filter->($self, $value); } else { $self->{'output_value'} = $value } if(wantarray && ref $self->{'output_value'} eq 'ARRAY') { return @{$self->{'output_value'}}; } return $self->{'output_value'}; } sub is_empty { my($self) = shift; my $value = $self->internal_value; no warnings; return ($value =~ /\S/ || (!$self->trim_spaces && length $value)) ? 0 : 1; } sub is_full { !shift->is_empty } sub input_prefilter { my($self, $value) = @_; return undef unless(defined $value); unless(ref $value) { for($value) { no warnings; if($self->trim_spaces) { s/^\s+//; s/\s+$//; } } } return $value; } sub input_filter { my($self) = shift; if(@_) { $self->{'internal_value'} = undef; $self->{'output_value'} = undef; return $self->{'input_filter'} = shift; } return $self->{'input_filter'}; } sub output_filter { my($self) = shift; if(@_) { $self->{'output_value'} = undef; return $self->{'output_filter'} = shift; } return $self->{'output_filter'}; } sub filter { my($self) = shift; if(@_) { my $filter = shift; $self->{'input_filter'} = $filter; $self->{'output_filter'} = $filter; } my $input_filter = $self->{'input_filter'}; if(ref $input_filter && $input_filter eq $self->output_filter) { return $input_filter; } return; } sub clear { my($self) = shift; $self->_set_input_value(undef); $self->error(undef); $self->has_partial_value(0); $self->is_cleared(1); } sub reset { my($self) = shift; $self->_set_input_value(undef); $self->error(undef); $self->has_partial_value(0); $self->is_cleared(0); return 1; } sub hidden_fields { my($self) = shift; require Rose::HTML::Form::Field::Hidden; # Circular dependency... :-/ return Rose::HTML::Form::Field::Hidden->new( name => $self->html_attr('name'), id => $self->html_attr('id'), class => $self->html_attr('class'), value => $self->output_value); } sub hidden_field { shift->hidden_fields(@_) } sub html_hidden_fields { my($self) = shift; my @html; foreach my $field ($self->hidden_fields) { push(@html, $field->html_field); } return (wantarray) ? @html : join("\n", @html); } sub html_hidden_field { shift->html_hidden_fields(@_) } sub xhtml_hidden_fields { my($self) = shift; my @xhtml; foreach my $field ($self->hidden_fields) { push(@xhtml, $field->xhtml_field); } return (wantarray) ? @xhtml : join("\n", @xhtml); } sub xhtml_hidden_field { shift->xhtml_hidden_fields(@_) } sub element { my($self) = shift; Carp::croak "Cannot set element for ", ref($self) if(@_); return $self->html_element; } sub html_tag { my($self) = shift; if($self->html_element && $self->apply_error_class && defined $self->error) { my $class = $self->html_attr('class'); $self->html_attr(class => $class ? "$class error" : 'error'); my $html = $self->Rose::HTML::Object::html_tag(@_); $self->html_attr(class => $class); return $html; } else { $self->SUPER::html_tag(@_); } } sub xhtml_tag { my($self) = shift; if($self->html_element && $self->apply_error_class && defined $self->error) { my $class = $self->html_attr('class'); $self->html_attr(class => $class ? "$class error" : 'error'); my $html = $self->Rose::HTML::Object::xhtml_tag(@_); $self->html_attr(class => $class); return $html; } else { $self->SUPER::xhtml_tag(@_); } } *html_field = \&html_tag; *xhtml_field = \&xhtml_tag; sub html { my($self) = shift; my($field, $error); $field = $self->html_field; $error = $self->html_error; if($error) { return $field . $self->html_error_separator . $error; } return $field; } sub xhtml { my($self) = shift; my($field, $error); $field = $self->xhtml_field; $error = $self->xhtml_error; if($error) { return $field . $self->xhtml_error_separator . $error; } return $field; } # sub label # { # # } sub label_object { my($self) = shift; my $label = $self->{'label_object'} ||= Rose::HTML::Label->new(); $label->contents($self->escape_html ? __escape_html($self->label) : $self->label); if($self->html_attr_exists('id')) { $label->for($self->html_attr('id')); } my @classes = ( ($self->required ? 'required' : ()), (defined $self->error ? 'error' : ()), ); if(@classes) { $label->html_attr(class => "@classes"); } if(@_) { my %args = @_; while(my($k, $v) = each(%args)) { $label->html_attr($k => $v); } } return $label; } sub html_label { my($self) = shift; no warnings 'uninitialized'; return '' unless(length $self->label); return $self->label_object(@_)->html_tag; } sub xhtml_label { my($self) = shift; no warnings 'uninitialized'; return '' unless(length $self->label); return $self->label_object(@_)->xhtml_tag; } sub validate { my($self) = shift; $self->error(undef); my $value = $self->internal_value; if($self->required && ((!ref $value && (!defined $value || ($self->trim_spaces && $value !~ /\S/))) || (ref $value eq 'ARRAY' && !@$value))) { unless($self->is_empty && $self->empty_is_ok) { my $label = $self->error_label; if(defined $label) { #$self->add_error_id(FIELD_REQUIRED, $label); #$self->add_error_id(FIELD_REQUIRED, [ $label ]); $self->add_error_id(FIELD_REQUIRED, { label => $label }); } else { $self->add_error_id(FIELD_REQUIRED); } return 0; } } my $code = $self->validator; if($code) { local $_ = $value; #$Debug && warn "running $code->($self)\n"; my $ok = $code->($self); if(!$ok && !$self->has_errors) { my $label = $self->label; if(defined $label) { $self->add_error_id(FIELD_INVALID, { label => $label }) } else { $self->add_error_id(FIELD_INVALID); } } return $ok; } return 1; } sub validator { my($self) = shift; if(@_) { my $code = shift; if(ref $code eq 'CODE') { return $self->{'validator'} = $code; } else { Carp::croak ref($self), "::validator() - argument must be a code reference"; } } return $self->{'validator'}; } *__escape_html = \&Rose::HTML::Util::escape_html; sub message_for_error_id { my($self, %args) = @_; my $error_id = $args{'error_id'}; my $msg_class = $args{'msg_class'}; my $args = $args{'args'} || []; no warnings 'uninitialized'; if($error_id == FIELD_REQUIRED) { my $msg = $msg_class->new(args => $args); if((ref $args eq 'HASH' && keys %$args) || (ref $args eq 'ARRAY' && @$args)) { $msg->id(FIELD_REQUIRED_LABELLED); } else { $msg->id(FIELD_REQUIRED_GENERIC); } return $msg; } elsif($error_id == FIELD_INVALID) { my $msg = $msg_class->new(args => $args); if((ref $args eq 'HASH' && keys %$args) || (ref $args eq 'ARRAY' && @$args)) { $msg->id(FIELD_INVALID_LABELLED); } else { $msg->id(FIELD_INVALID_GENERIC); } return $msg; } return undef; } sub localize_label { shift->label_message_id(FIELD_LABEL) } sub localize_description { shift->description_message_id(FIELD_DESCRIPTION) } # XXX: Ths sub contains a lame hack to work around an incompatibility with # XXX: Scalar::Defer 0.11 - it manually detects and evaluates code refs. sub localizer { my($invocant) = shift; # Called as object method if(my $class = ref $invocant) { if(@_) { $invocant->{'localizer'} = shift; if(ref $invocant->{'localizer'} eq 'CODE') { return $invocant->{'localizer'}->(); } else { return $invocant->{'localizer'}; } } my $localizer = $invocant->{'localizer'}; unless($localizer) { if(my $parent_field = $invocant->parent_field) { if(my $localizer = $parent_field->localizer) { return (ref $localizer eq 'CODE') ? $localizer->() : $localizer; } } elsif(my $parent_form = $invocant->parent_form) { if(my $localizer = $parent_form->localizer) { return (ref $localizer eq 'CODE') ? $localizer->() : $localizer; } } else { return $class->default_localizer } } if(ref $localizer eq 'CODE') { return $localizer->(); } return $localizer || $class->default_localizer; } else # Called as class method { if(@_) { return $invocant->default_localizer(shift); } return $invocant->default_localizer; } } # XXX: Ths sub contains a lame hack to work around an incompatibility with # XXX: Scalar::Defer 0.11 - it manually detects and evaluates code refs. sub locale { my($invocant) = shift; # Called as object method if(my $class = ref $invocant) { if(@_) { $invocant->{'locale'} = shift; if(ref $invocant->{'locale'} eq 'CODE') { return $invocant->{'locale'}->(); } else { return $invocant->{'locale'}; } } my $locale = $invocant->{'locale'}; if($locale) { return (ref $locale eq 'CODE') ? $locale->() : $locale; } if(my $parent_field = $invocant->parent_field) { if(my $locale = $parent_field->locale) { return (ref $locale eq 'CODE') ? $locale->() : $locale; } } elsif(my $parent_form = $invocant->parent_form) { if(my $locale = $parent_form->locale) { return (ref $locale eq 'CODE') ? $locale->() : $locale; } } else { my $locale = $invocant->localizer->locale; if(ref $locale eq 'CODE') { return $locale->(); } return $locale || $class->default_locale; } } else # Called as class method { if(@_) { return $invocant->default_locale(shift); } return $invocant->default_locale; } } sub prepare { } if(__PACKAGE__->localizer->auto_load_messages) { __PACKAGE__->localizer->load_all_messages; } 1; __DATA__ [% LOCALE en %] FIELD_LABEL = "" FIELD_DESCRIPTION = "" FIELD_REQUIRED_GENERIC = "This is a required field." FIELD_REQUIRED_LABELLED = "[1] is a required field." FIELD_PARTIAL_VALUE = "Incomplete value." FIELD_INVALID_GENERIC = "Value is invalid." FIELD_INVALID_LABELLED = "[label] is invalid." [% LOCALE de %] FIELD_REQUIRED_GENERIC = "Dies ist ein Pflichtfeld." FIELD_REQUIRED_LABELLED = "[1] ist ein Pflichtfeld." # ganze Sätze oder nur "Wert unvollständig/ungültig"? FIELD_PARTIAL_VALUE = "Der Wert ist unvollständig." FIELD_INVALID_GENERIC = "Der Wert ist ungültig." FIELD_INVALID_LABELLED = "[label] ist ungültig." [% LOCALE fr %] FIELD_REQUIRED_GENERIC = "Ce champ est obligatoire." FIELD_REQUIRED_LABELLED = "[1] est un champ obligatoire." FIELD_PARTIAL_VALUE = "Valeur incomplète." FIELD_INVALID_GENERIC = "Valeur invalide." FIELD_INVALID_LABELLED = "[label] est invalide." [% LOCALE bg %] FIELD_REQUIRED_GENERIC = "Това поле е задължително." FIELD_REQUIRED_LABELLED = "Полето '[1]' е задължително." FIELD_PARTIAL_VALUE = "Непълна стойност." FIELD_INVALID_GENERIC = "Стойността е невалидна." FIELD_INVALID_LABELLED = "Полето '[label]' е невалидно." __END__ =head1 NAME Rose::HTML::Form::Field - HTML form field base class. =head1 SYNOPSIS package MyField; use Rose::HTML::Form::Field; our @ISA = qw(Rose::HTML::Form::Field); ... my $f = MyField->new(name => 'test', label => 'Test'); print $f->html_field; print $f->xhtml_field; $f->input_value('hello world'); $i = $f->internal_value; print $f->output_value; ... =head1 DESCRIPTION L is the base class for field objects used in an HTML form. It defines a generic interface for field input, output, validation, and filtering. This class inherits from, and follows the conventions of, L. Inherited methods that are not overridden will not be documented a second time here. See the L documentation for more information. =head1 OVERVIEW A field object provides an interface for a logical field in an HTML form. Although it may serialize to multiple HTML tags, the field object itself is a single, logical entity. L is the base class for field objects. Since the field object will eventually be asked to serialize itself as HTML, L inherits from L. That defines a lot of a field object's interface, leaving only the field-specific functions to L itself. The most important function of a field object is to accept and return user input. L defines a data flow for field values with several different hooks and callbacks along the way: +------------+ / user input / +------------+ | V +------------------+ set -->. . . input_value . input_value() get <--. . +------------------+ | V +------------------+ toggle -->| input_prefilter | trim_spaces() +------------------+ | V +------------------+ define <-->| input_filter | input_filter() +------------------+ | V +----------------------+ . . get <--. input_value_filtered . input_value_filtered() . . +----------------------+ | V +------------------+ | inflate_value | (override in subclass) +------------------+ | V +------------------+ . . get <--. internal_value . internal_value() . . +------------------+ | V +------------------+ | deflate_value | (override in subclass) +------------------+ | V +------------------+ define <-->| output_filter | output_filter() +------------------+ | V +------------------+ . . get <--. output_value . output_value() . . +------------------+ Input must be done "at the top", by calling L. The value as it exists at various stages of the flow can be retrieved, but it can only be set at the top. Input and output filters can be defined, but none exist by default. The purposes of the various stages of the data flow are as follows: =over 4 =item B The value as it was passed to the field. =item B The input value after being passed through all input filters, but before being inflated. =item B The most useful representation of the value as far as the user of the L-derived class is concerned. It has been filtered and optionally "inflated" into a richer representation (i.e., an object). The internal value must also be a valid input value. =item B The value as it will be used in the serialized HTML representation of the field, as well as in the equivalent URI query string. This is the internal value after being optionally "deflated" and then passed through an output filter. This value should be a string or a reference to an arry of strings. If passed back into the field as the input value, it should result in the same output value. =back Only subclasses can define class-wide "inflate" and "deflate" methods (by overriding the no-op implementations in this class), but users can define input and output filters on a per-object basis by passing code references to the appropriate object methods. The prefilter exists to handle common filtering tasks without hogging the lone input filter spot (or requiring users to constantly set input filters for every field). The L prefilter optionally trims leading and trailing whitespace based on the value of the L boolean attribute. This is part of the public API for field objects, so subclasses that override L must preserve this functionality. In addition to the various kinds of field values, each field also has a name, which may or may not be the same as the value of the "name" HTML attribute. Fields also have associated labels, error strings, default values, and various methods for testing, clearing, and reseting the field value. See the list of object methods below for the details. =head1 HIERARCHY Though L objects inherit from L, there are some semantic differences when it comes to the L of parent/child objects. A field is an abstraction for a collection of one or more HTML tags, including the field itself, the field's L, and any L. Each of these things may be made up of multiple HTML elements, and they usually exist alongside each other rather than nested within each other. As such, the field itself cannot rightly be considered the "parent" of these elements. This is why the child-related methods inherited from L (L, L, etc.) will usually return empty lists. Furthermore, any children L to the list will generally be ignored by the field's HTML output methods. Effectively, once we move away from the L-derived classes that represent a single HTML element (with zero or more children nested within it) to a class that presents a higher-level abstraction, such as a L or field, the exact population of and relationship between the constituent HTML elements may be opaque. If a field is a group of sibling HTML elements with no real parent HTML element (e.g., a L), then the individual sibling items will be available through a dedicated method (e.g., L). In cases where there really is a clear parent/child relationship among the HTML elements that make up a field, such as a L