package HTML::FormWidgets; # @(#)$Id: FormWidgets.pm 41 2008-05-24 23:04:10Z pjf $ use strict; use warnings; use base qw(Class::Data::Accessor); use English qw(-no_match_vars); use File::Spec::Functions; use HTML::Accessors; use Readonly; use version; our $VERSION = qv( sprintf '0.1.%d', q$Rev: 41 $ =~ /\d+/gmx ); Readonly my $NUL => q(); Readonly my $TTS => q( ~ ); Readonly my %ATTRS => ( ajaxid => undef, ajaxtext => undef, align => q(left), all => [], assets => $NUL, atitle => 'All', base => $NUL, behaviour => q(classic), button => $NUL, checked => 0, class => $NUL, clear => $NUL, columns => undef, container => undef, content_type => q(application/xhtml+xml), ctitle => 'Current', current => [], data => {}, default => undef, dropcap => 0, edit => 0, elem => undef, evnt_hndlr => 'checkObj.CheckField', field => $NUL, fields => {}, form => {}, 'format' => undef, fhelp => $NUL, header => undef, height => undef, hide => [], hint_title => 'Handy Hint', href => undef, id => undef, id2key => {}, key => $NUL, key2id => {}, key2url => {}, labels => undef, max_length => undef, messages => undef, name => $NUL, nb_symbol => q( †), node => undef, nowrap => 0, onblur => undef, onchange => undef, onclick => undef, onkeypress => undef, palign => undef, path => undef, prompt => $NUL, fields => {}, pwidth => 40, required => 0, root => undef, select => undef, sep => q( : ), space => q( ) x 3, stepno => undef, style => $NUL, subtype => undef, swidth => 1000, tabstop => 3, target => $NUL, templatedir => undef, text => $NUL, tip => $NUL, tiptype => q(dagger), title => $NUL, type => undef, url => undef, value => 1, values => [], width => undef, ); Readonly my @STATIC => ( qw(atitle align behaviour checked class clear container ctitle edit fhelp format height hint_title max_length max_value min_length min_value nowrap onchange onkeypress palign prompt pwidth required select sep stepno subtype tabstop text tip tiptype width) ); __PACKAGE__->mk_classaccessors( keys %ATTRS ); # Class methods sub build { my ($me, $config, $form) = @_; my ($item, $list, $ref, @tmp, $widget); for $list (@{ $form }) { next unless ($list && ref $list eq q(HASH)); @tmp = (); for $item (@{ $list->{items} }) { if (ref $item->{content} eq q(HASH)) { if ($item->{content}->{group}) { $ref = { content => $me->_group_fields( $item, \@tmp ) }; } elsif ($item->{content}->{widget}) { $widget = $me->new( $me->_merge_config( $config, $item ) ); $ref = { content => $widget->render }; $ref->{class} = $widget->class if ($widget->class); } else { $ref = $item->{content} } } else { $ref = { content => $item->{content} } } $ref->{rownum} = $item->{rownum} if (defined $item->{rownum}); push @tmp, $ref; } @{ $list->{items} } = @tmp; } return; } sub new { my ($me, @rest) = @_; my $args = $me->_arg_list( @rest ); my ($class, $method, $msg_id, $ref, $self, $text, @tmp, $val); # Start with some hard coded defaults; $self = { %ATTRS }; # Now we can create HTML elements like we could with CGI.pm $ref = { content_type => $args->{content_type} } if ($args->{content_type}); $self->{elem} = HTML::Accessors->new( $ref ); # Bare minimum is fields + id to get a useful widget for (qw(ajaxid fields id name)) { $self->{ $_ } = $args->{ $_ } if (exists $args->{ $_ }); } # Defaults id from name (least significant) from id from ajaxid (most sig.) $self->{id} = $self->{ajaxid} if (!$self->{id} && $self->{ajaxid}); if (!$self->{name} && $self->{id}) { if ($self->{id} =~ m{ \. }mx) { (undef, $self->{name}) = split m{ \. }mx, $self->{id}; } else { ($self->{name}) = reverse split m{ _ }mx, $self->{id} } } $self->{id} = $self->{name} if (!$self->{id} && $self->{name}); # Get static attributes for this id from the fields passed in $args if ($self->{id} && $self->{fields} && defined $self->{fields}->{ $self->{id} }) { for (@STATIC) { if (defined( $val = $self->{fields}->{ $self->{id} }->{ $_ } )) { $self->{ $_ } = $val; } } } # Passed args override XML config for (grep { exists $self->{ lc $_ } } keys %{ $args }) { $self->{ lc $_ } = $args->{ $_ }; } # We can get the widget type from the fields in level.xml if ( ! $self->{type} && $self->{id} && $self->{fields} && $self->{fields}->{ $self->{id} } && $self->{fields}->{ $self->{id} }->{type}) { $self->{type} = $self->{fields}->{ $self->{id} }->{type}; } $self->{type} = q(textfield) unless ($self->{type}); # Your basic factory method trick $class = __PACKAGE__.q(::).(ucfirst $self->{type}); ## no critic eval "require $class;"; ## critic if ($EVAL_ERROR) { $self->{text} = $EVAL_ERROR; $self->{type} = undef; } bless $self, $class; $self->{nodeId} = q(node_0); # Define accessor by hand to auto increment # Pander to lazy filling out of static definitions $self->container( $self->type =~ m{ chooser|file|label|note }mx ? 0 : 1 ) unless (defined $self->container); if ($self->ajaxid) { $msg_id = $self->fields ? $self->fields->{ $self->ajaxid }->{validate} : $NUL; $msg_id = $msg_id->[0] if (ref $msg_id eq q(ARRAY)); $text = $self->msg( $msg_id ) || 'Invalid field value'; $self->ajaxtext( $text ); # Install default JavaScript event handler unless ($self->onblur || $self->onchange || $self->onkeypress) { $text = $self->evnt_hndlr.'(\''.$self->ajaxid.'\', this.value)'; $self->onblur( $text ); } } $self->hint_title( $text ) if ($text = $self->msg( q(handy_hint_title) )); unless (defined $self->height) { $self->height( $self->type eq q(groupMembership) || $self->type eq q(scrollingList) ? 10 : 5 ); } if ($self->pwidth && ($self->pwidth =~ m{ \A \d+ \z }mx)) { $self->pwidth( (int $self->pwidth * $self->swidth / 100).q(px) ); } $self->sep( $NUL ) if ($self->type eq q(note)); $self->sep( $NUL ) if (!$self->prompt && !$self->fhelp); $self->sep( $NUL ) if ($self->sep =~ m{ \A \d+ \z }mx && $self->sep == 0); $self->sep( $self->space ) if ($self->sep && $self->sep eq q(space)); if (defined $self->stepno && $self->stepno == 0) { $self->stepno( $self->space ); } if ($self->stepno && $self->stepno ne $self->space) { $self->stepno( $self->stepno.q(.) ); } return $self; } # Object methods sub msg { my ($me, $key) = @_; return q() unless ($me->messages); my $msg = $me->messages->{ $key || q() } || {}; return $msg->{text} || q(); } sub render { my $me = shift; my ($field, $htag, $html, $method, $ref, $tip); return $me->text || $NUL unless ($me->type); $htag = $me->elem; $html = $me->clear eq q(left) ? $htag->br() : "\n"; if ($me->stepno) { $html .= $htag->span( { class => q(lineNumber) }, $me->stepno ); } if ($me->prompt) { $ref = { class => q(prompt) }; $ref->{for } = $me->id if ($me->id); $ref->{style} .= 'text-align: '.$me->palign.'; ' if ($me->palign); $ref->{style} .= 'white-space: nowrap; ' if ($me->nowrap); $ref->{style} .= 'width: '.$me->pwidth.q(;) if ($me->pwidth); $html .= $htag->label( $ref, $me->prompt ); } if ($me->type eq q(groupMembership)) { $ref = { class => q(instructions) }; $ref->{style} .= 'text-align: '.$me->palign.'; ' if ($me->palign); $ref->{style} .= 'width: '.$me->pwidth.q(;) if ($me->pwidth); $html .= $htag->div( $ref, $me->fhelp ); } $html .= $htag->div( { class => q(separator) }, $me->sep ) if ($me->sep); $ref = {}; $ref->{class } = q(required) if ($me->required); $ref->{default } = $me->default if ($me->default); $ref->{id } = $me->id if ($me->id); $ref->{name } = $me->name if ($me->name); $ref->{onblur } = $me->onblur if ($me->onblur); $ref->{onkeypress} = $me->onkeypress if ($me->onkeypress); return $html unless ($field = $me->_render( $ref )); if ($me->container) { $field = $htag->div( { class => q(container ).$me->align }, $field ); } if ($tip = $me->tip and $me->type ne q(imageButton)) { $tip =~ s{ \n }{ }gmx; $tip = $me->hint_title.$TTS.$tip if ($tip !~ m{ $TTS }mx); $tip =~ s{ \s+ }{ }gmx; $ref = { class => q(help tips), title => $tip }; if ($me->tiptype ne q(dagger)) { $field = $htag->span( $ref, $field ) } else { $field .= $htag->span( $ref, $me->nb_symbol ) } $field = $htag->div( { class => q(container) }, $field ); } if ($me->ajaxid) { $ref = { class => q(hidden), id => $me->ajaxid.q(_checkField) }; $field .= $htag->span( $ref, $me->ajaxtext ); $field = $htag->div( { class => q(container) }, $field ); } return $html.$field; } # Private methods sub _arg_list { my ($me, @rest) = @_; return {} unless ($rest[0]); return ref $rest[0] eq q(HASH) ? $rest[0] : { @rest }; } sub _group_fields { my ($me, $item, $list) = @_; my $html = $NUL; my $ref; for (1 .. $item->{content}->{nitems}) { $ref = pop @{ $list }; chomp $ref->{content}; $html = $ref->{content}.$html; } my $htag = HTML::Accessors->new(); my $legend = $htag->legend( $item->{content}->{text} ); return $htag->fieldset( $legend.$html ); } sub _merge_config { my ($me, $config, $item) = @_; return { %{ $config }, %{ $item->{content} } }; } sub _render { my ($me, $ref) = @_; return $me->text if ($me->text); return 'No _render method for field '.($ref->{id} || '*unknown id*'); } 1; __END__ =pod =head1 Name HTML::FormWidgets - Create HTML form markup =head1 Version 0.1.$Rev: 41 $ =head1 Synopsis package MyApp::View::HTML; use base qw(CatalystX::Usul::View::HTML); use HTML::FormWidgets; sub build_form { my ($me, $c) = @_; my $s = $c->stash; my $form = [ $s->{iFrame} ]; my $config = {}; $config->{root } = $c->config->{root}; $config->{base } = $c->req->base; $config->{content_type} = $c->config->{content_type}; $config->{url } = $c->req->path; $config->{assets } = $s->{assets}; $config->{fields } = $s->{fields} || {}; $config->{form } = $s->{form}; $config->{hide } = $s->{iFrame}->{hidden}; $config->{messages } = $s->{messages}; $config->{swidth } = $s->{width} if ($s->{width}); $config->{templatedir } = $c->config->{dynamic_templates}; HTML::FormWidgets->build( $config, $form ); return; } =head1 Description Transforms a Perl data structure which defines one or more "widgets" into HTML or XHTML. Each widget is comprised of these optional components: a line or question number, a prompt string, a separator, an input field, additional field help, and Ajax field error string. Input fields are selected by the widget C attribute. A factory subclass implements the method that generates the HTML or XHTML for that input field type. Adding more widget types is straightforward This module is using the MooTools Javascript library L to modify default browser behaviour This module is used by L and as such its main use is a form generator within a L application =head1 Subroutines/Methods =head2 build The C method iterates over a data structure that represents the form. One or more lists of widgets are processed in turn. New widgets are created and their rendered output replaces their definitions in the data structure =head2 new Construct a widget. Mostly this is called by the C method. It requires the factory subclass for the widget type. This method takes a large number of options with each widget using only few of them. Each option is described in the factory subclasses which use that option =head2 msg Use the supplied key to return a value from the C<$me-Emessages> hash. This hash was passed to the constructor and should contain any literal text used by any of the widgets =head2 render Assemble the components of the generated widget. Each component is concatenated onto a scalar which is the returned value. This method calls C<_render> which should be defined in the factory subclass for this widget type. This method uses these attributes: =over 3 =item C<$me-Eclear> If set to B the widget begins with an
element =item C<$me-Estepno> If true it's value is wrapped in a B element of class B and appended to the return value =item C<$me-Eprompt> If true it's value is wrapped in a B