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