todo
[perl/html-form-simple.git] / lib / HTML / Form / Simple.pm
1 package HTML::Form::Simple;
2
3 use strict;
4 use warnings;
5
6 use XML::Quote;
7
8 our $VERSION = '1.00';
9
10
11 sub new {
12         my ($class, $default) = @_;
13         bless {default => $default}, $class;
14 }
15
16 sub _attr {
17         my $self = shift;
18         my $expect = shift;
19         my $attr = ref $_[-1] eq 'HASH' ? pop : {};
20         push @_, undef for @_+1 .. $expect;
21         push @_, $attr;
22         return @_;
23 }
24
25 sub quote {
26         my $self = shift;
27         return XML::Quote::xml_quote_min($_[0]);
28 }
29
30 sub tag {
31         my ($self, $tag, $attr) = @_;
32
33         # strip empty if it shouldn't be
34         defined $attr->{$_} and $attr->{$_} eq '' and delete $attr->{$_}
35                 for qw(id type class style);
36
37         my $return = '<' . $tag;
38
39         # add booleans
40         delete $attr->{$_} and $return .= ' '.$_
41                 for qw(selected checked disabled readonly);
42
43         $return .= sprintf ' %s="%s"', $_, $self->quote($attr->{$_})
44                 for sort grep { defined $attr->{$_} } keys %$attr;
45
46         return $return . '>';
47 }
48
49
50 sub start {
51         my ($self, $attr) = @_;
52
53         return $self->tag(form => $attr);
54 }
55
56 sub stop {
57         return '</form>';
58 }
59
60
61 sub submit {
62         my $self = shift;
63         my ($value, $attr) = $self->_attr(1, @_);
64
65         $attr->{value} = $value if defined $value;
66         $attr->{type} = 'submit' unless defined $attr->{type};
67
68         return $self->tag(input => $attr);
69 }
70
71 sub hidden {
72         my $self = shift;
73         my ($name, $value, $attr) = $self->_attr(2, @_);
74
75         if (ref $name eq 'HASH') {
76                 my @return = map { $self->hidden($_, $name->{$_}, $attr) } sort keys %$name;
77                 return wantarray ? @return : join(defined $, ? $, : '', @return);
78         }
79
80         if (ref $value eq 'ARRAY') {
81                 my @return = map { $self->hidden($name, $_, $attr) } @$value;
82                 return wantarray ? @return : join(defined $, ? $, : '', @return);
83         }
84
85         $attr->{name } = $name  if defined $name;
86         $attr->{value} = $value if defined $value;
87         $attr->{value} = $self->{default}->{$name}
88                 if not defined $attr->{value} and defined $name and defined $self->{default};
89         $attr->{type} = 'hidden' unless defined $attr->{type};
90
91         return $self->tag(input => $attr);
92 }
93
94 sub text {
95         my $self = shift;
96         my ($name, $value, $attr) = $self->_attr(2, @_);
97
98         $attr->{name } = $name  if defined $name;
99         $attr->{value} = $value if defined $value;
100         $attr->{value} = $self->{default}->{$name}
101                 if not defined $attr->{value} and defined $name and defined $self->{default};
102         $attr->{id}   = $attr->{name} unless defined $attr->{id};
103         $attr->{type} = 'text' unless defined $attr->{type} or defined $attr->{rows};
104         $value = delete $attr->{value} if defined $attr->{rows};
105
106         return defined $attr->{rows} ? sprintf(
107                 '%s%s</textarea>',
108                 $self->tag(textarea => $attr),
109                 $self->quote(defined $value ? $value : '')
110         ) : $self->tag(input => $attr);
111 }
112
113 sub select {
114         my $self = shift;
115         my ($name, $rows, $default, $attr) = $self->_attr(3, @_);
116
117         $attr->{name} = $name;
118         $attr->{id}   = $attr->{name} unless defined $attr->{id};
119         $attr->{type} = 'select' unless defined $attr->{type};
120
121         $default = $attr->{value} unless defined $default;
122         delete $attr->{value};  # never a parent attribute
123         $default = $self->{default}->{$name}
124                 if not defined $default and defined $name and defined $self->{default};
125
126         my @options = map { ref $_ ? $_ : {value => $_} } @$rows;
127
128         my @return;
129
130         if ($attr->{type} eq 'select') {
131                 delete $attr->{type};
132                 if (defined $default) {
133                         for (@options) {
134                                 $_->{selected} = 1 if defined $_->{value} and $_->{value} eq $default;
135                         }
136                 }
137                 @return = (
138                         $self->tag(select => $attr),
139                         (map { $self->tag(option => $_) } @options),
140                         '</select>',
141                 );
142         }
143         else {
144                 if (defined $attr->{id} and $attr->{id} ne '') {
145                         defined $_->{id}
146                                 or defined $_->{value} and $_->{id} = $attr->{id}.'_'.$_->{value}
147                                         for @options;
148                 }
149                 if (defined $attr->{label}) {
150                         defined $_->{value} and not defined $_->{label}
151                                 and $_->{label} = $attr->{label}->{$_->{value}}
152                                         for @options;
153                         delete $attr->{label};
154                 }
155                 if (defined $default) {
156                         for (@options) {
157                                 $_->{checked} = 1 if defined $_->{value} and $_->{value} eq $default;
158                         }
159                 }
160                 $_ = {%$attr, %$_} for @options;
161                 @return = map {
162                         my $label = delete $_->{label};
163                         defined $label && $label ne ''
164                                 ? '<label>'.$self->tag(input => $_)." $label</label>"
165                                 :           $self->tag(input => $_)
166                 } @options;
167         }
168
169         return wantarray ? @return : join(defined $, ? $, : '', @return);
170 }
171
172 sub radio {
173         my $self = shift;
174         my ($name, $rows, $label, $attr) = $self->_attr(3, @_);
175
176         if (not defined $rows) {
177                 if (defined $label) {
178                         $rows = ref $label eq 'ARRAY' ? [1 .. $#$label+1] : [1];
179                 }
180                 else {
181                         $rows = [{}];
182                 }
183         }
184         elsif (ref $rows ne 'ARRAY') {
185                 $rows = [$rows];
186         }
187
188         if (defined $label) {
189                 $rows = [ map { ref $_ eq 'HASH' ? {%$_} : {value => $_} } @$rows ];
190                 if (ref $label eq 'ARRAY') {
191                         $rows->[$_]->{label} = $label->[$_] for 0 .. $#$rows;
192                 } else {
193                         $_->{label} = $label for @$rows;
194                 }
195         }
196
197         $self->select($name, $rows, {%$attr, type => 'radio'});
198 }
199
200 sub check {
201         my $self = shift;
202         my ($name, $label, $checked, $attr) = $self->_attr(3, @_);
203
204         my $rows = defined $label ? ref $label eq 'ARRAY' ? [@$label] : [$label] : [{}];
205         ref $_ eq 'HASH' or $_ = {label => $_} for @$rows;
206         if (defined $checked) {
207                 if (ref $checked eq 'ARRAY') {
208                         $rows->[$_]->{checked} = $checked->[$_] for 0 .. $#$rows;
209                         push @$rows, map { {checked => $_} } @$checked[@$rows .. $#$checked];
210                 }
211                 else {
212                         $_->{checked} = $checked for @$rows;
213                 }
214         }
215         exists $rows->[$_]->{value} or $rows->[$_]->{value} = $_ + 1 for 0 .. $#$rows;
216
217         $self->select($name, $rows, {%$attr, type => 'checkbox'});
218 }
219
220 1;
221
222 __END__
223
224 =head1 NAME
225
226 HTML::Form::Simple - Generate HTML form elements
227
228 =head1 SYNOPSIS
229
230         my $input = HTML::Form::Simple->new;
231         say $input->start; # <form>
232         printf "<label>%s: %s</label>\n", @$_ for (
233                 [ 'Your Name' => $input->text(
234                         user => 'Mr. Default'
235                 ) ],
236                 [ Message => $input->text(
237                         msg => 'Textarea default', {rows => 4, style => 'background:red'}
238                 ) ],
239                 [ Gender => join ' or ', $input->radio(
240                         sex => [qw(m f)], [qw(Male Female)]
241                 ) ],
242                 [ Colour => scalar $input->select(
243                         favcolour => [qw(Blue Green Red)], 'Green'
244                 ) ],
245                 [ Options => $input->check(
246                         spam => 'Receive weekly newsletter'
247                 ) ],
248         );
249         say $input->stop; # </form>
250
251 =head1 TODO
252
253 =over
254
255 =item C<default()> method
256
257         $input->hidden(foo => $input->default('foo'));
258         $hash_ref = $input->default;
259         $input->default('foo') = 'new value';
260         undef $input->default;  # clear all
261                 # XXX: does this equal $input->default=undef;?
262         $input->default = {amend => 'stuff'};
263
264 =item documentation
265
266 Actual descriptions instead of just a synopsis.
267
268 =item C<quote> override
269
270 Allow custom value quotation function.
271 Makes L<XML::Quote|XML::Quote> dependency optional.
272
273 =item single checkbox id
274
275 Do not add value to single check() by default.
276
277 =back
278
279 =head1 AUTHOR
280
281 Mischa POSLAWSKY <perl@shiar.org>
282
283 =head1 LICENSE
284
285 This module is free software; you can redistribute it and/or modify it
286 under the same L<terms|perlartistic> as Perl itself.
287