File Coverage

blib/lib/Form/Diva.pm
Criterion Covered Total %
statement 269 270 99.6
branch 90 92 97.8
condition 20 23 86.9
subroutine 24 24 100.0
pod 8 8 100.0
total 411 417 98.5


line stmt bran cond sub pod time code
1 16     16   10996 use strict;
  16         31  
  16         458  
2 16     16   70 use warnings;
  16         29  
  16         423  
3 16     16   66 no warnings 'uninitialized';
  16         25  
  16         1069  
4              
5             package Form::Diva;
6              
7             our $VERSION='1.05';
8              
9             # use Data::Printer;
10              
11             # ABSTRACT: Generate HTML5 form label and input fields
12              
13 16     16   583 use Storable 3.15 qw(dclone);
  16         3781  
  16         42892  
14              
15             # The _option_id sub needs access to a variable for hashing the ids
16             # in use, even though it is initialized at the beginning of generate,
17             # it needs to exist outside of the generate subroutines scope
18             # and before before the _option_id sub is declared.
19             my %id_uq = ();
20 36     36   4692 sub _clear_id_uq { %id_uq = () }
21              
22             # our $id_base = 'formdiva_';
23              
24             # True if all fields are used no more than once, if not it dies.
25             # Form::Diva->{FormHash} stores all the fields a duplicated fieldname
26             # would replace the previous value.
27             sub _field_once {
28 35     35   1231 my $self = shift;
29 35         40 my @fields = ( @{ $self->{FormMap} }, @{ $self->{HiddenMap} } );
  35         69  
  35         87  
30 35         58 my %hash = ();
31 35         57 foreach my $field (@fields) {
32 154 100       232 if ( $hash{ $field->{name} } ) {
33 1         10 die "$field->{name} would appear more than once or "
34             . "is in both hidden and visible field lists. Not "
35             . "only would this cause strange behaviour in your form "
36             . "but it could internally corrupt Form::Diva";
37             }
38 153         225 else { $hash{ $field->{name} } = 1; }
39             }
40 34         71 return 1;
41             }
42              
43             sub _build_html_tag {
44 344     344   404 my $self = shift;
45 344         340 my $tag_name = shift;
46 344         600 my %options = @_;
47 344 100 50     366 my @attrs = grep { defined && length } @{ $options{attributes} // [] };
  1789         3348  
  344         675  
48              
49 344         717 my $output = join ' ', $tag_name, @attrs;
50 344         528 $output = "<$output>";
51              
52 344 100       498 if (exists $options{content}) {
53 153   50     341 $output .= join '', $options{content} // '', "";
54             }
55              
56 344 100       480 if (defined $options{prefix}) {
57 34         51 $output = $options{prefix} . $output;
58             }
59              
60 344 100       469 if (defined $options{suffix}) {
61 142         186 $output .= $options{suffix};
62             }
63              
64 344         1036 return $output;
65             }
66              
67             sub new {
68 32     32 1 22579 my $class = shift;
69 32         119 my $self = {@_};
70 32         54 bless $self, $class;
71 32         88 $self->{class} = $class;
72 32 100       104 unless ( $self->{input_class} ) { die 'input_class is required.' }
  1         8  
73 31 100       66 unless ( $self->{label_class} ) { die 'label_class is required.' }
  1         9  
74 30 100       118 $self->{id_base} = length $self->{id_base} ? $self->{id_base} : 'formdiva_';
75             ( $self->{HiddenMap}, my $HHash )
76 30         93 = $self->_expandshortcuts( $self->{hidden} );
77             ( $self->{FormMap}, my $FHash )
78 30         104 = $self->_expandshortcuts( $self->{form} );
79 29         45 $self->{FormHash} = { %{$HHash}, %{$FHash} };
  29         59  
  29         100  
80 29         107 $self->_field_once;
81 29         92 return $self;
82             }
83              
84             sub clone {
85 4     4 1 2087 my $self = shift;
86 4         5 my $args = shift;
87 4         7 my $new = {};
88 4         5 my $class = 'Form::Diva';
89 4         245 $new->{FormHash} = dclone $self->{FormHash};
90             $new->{input_class}
91 4 100       16 = $args->{input_class} ? $args->{input_class} : $self->{input_class};
92             $new->{label_class}
93 4 50       9 = $args->{label_class} ? $args->{label_class} : $self->{label_class};
94             $new->{form_name}
95 4 100       8 = $args->{form_name} ? $args->{form_name} : $self->{form_name};
96              
97 4 100       9 if ( $args->{neworder} ) {
98 3         3 my @reordered = map { $new->{FormHash}->{$_} } @{ $args->{neworder} };
  8         43  
  3         6  
99 3         8 $new->{FormMap} = \@reordered;
100             }
101 1         34 else { $new->{FormMap} = dclone $self->{FormMap}; }
102 4 100       8 if ( $args->{newhidden} ) {
103 1         2 my @hidden = map { $self->{FormHash}{$_} } @{ $args->{newhidden} };
  2         4  
  1         2  
104 1         2 $new->{HiddenMap} = \@hidden;
105             }
106 3         61 else { $new->{HiddenMap} = dclone $self->{HiddenMap}; }
107 4         8 bless $new, $class;
108 4         10 $self->_field_once;
109 4         8 return $new;
110             }
111              
112             # specification calls for single letter shortcuts on all fields
113             # these all need to expand to the long form.
114             sub _expandshortcuts {
115 64     64   436 my $self = shift;
116 64         103 my $FormMap = shift; # data passed to new
117 64         306 my %DivaShortMap = (
118             qw /
119             n name t type i id e extra x extra l label p placeholder
120             d default v values c class lc label_class /
121             );
122 64         187 my %DivaLongMap = map { $DivaShortMap{$_}, $_ } keys(%DivaShortMap);
  704         1081  
123 64         146 my $FormHash = {};
124 64         76 foreach my $formfield ( @{$FormMap} ) {
  64         132  
125 130         205 foreach my $tag ( keys %{$formfield} ) {
  130         289  
126 449 100       676 if ( $DivaShortMap{$tag} ) {
127             $formfield->{ $DivaShortMap{$tag} }
128 216         396 = delete $formfield->{$tag};
129             }
130             }
131 130 100       348 unless ( $formfield->{type} ) { $formfield->{type} = 'text' }
  23         48  
132 130 100       272 unless ( $formfield->{name} ) { die "fields must have names" }
  1         22  
133 129 100       246 unless ( $formfield->{id} ) {
134 101         248 $formfield->{id} = $self->{id_base} . $formfield->{name};
135             }
136              
137             # dclone because otherwise it would be a ref into FormMap
138 129         3477 $FormHash->{ $formfield->{name} } = dclone $formfield;
139             }
140 63         315 return ( $FormMap, $FormHash );
141             }
142              
143             sub input_class {
144 1     1 1 431 my $self = shift;
145 1         4 return $self->{input_class};
146             }
147              
148             sub label_class {
149 1     1 1 2 my $self = shift;
150 1         3 return $self->{label_class};
151             }
152              
153             # given a field returns either the default field class="string"
154             # or the field specific one
155             sub _class_input {
156 144     144   466 my $self = shift;
157 144         138 my $field = shift;
158 144   100     364 my $fclass = $field->{class} || '';
159 144 100       208 if ($fclass) { return qq!class="$fclass"! }
  19         45  
160 125         300 else { return qq!class="$self->{input_class}"! }
161             }
162              
163             sub _field_bits {
164 103     103   202 my $self = shift;
165 103         100 my $field_ref = shift;
166 103         125 my $data = shift;
167 103         103 my %in = %{$field_ref};
  103         305  
168 103         144 my %out = ();
169 103         117 my $fname = $in{name};
170 103         131 $out{extra} = $in{extra}; # extra is taken literally
171 103         154 $out{input_class} = $self->_class_input($field_ref);
172 103         178 $out{name} = qq!name="$in{name}"!;
173 103         150 $out{id} = qq!id="$in{id}"!;
174              
175 103 100       195 if ( lc( $in{type} ) eq 'textarea' ) {
176 4         5 $out{type} = 'textarea';
177 4         5 $out{textarea} = 1;
178             }
179             else {
180 99         155 $out{type} = qq!type="$in{type}"!;
181 99         108 $out{textarea} = 0;
182 99 50       153 if ( $in{type} eq 'hidden' ) { $out{hidden} = 1 }
  0         0  
183             }
184 103 100       152 if ( keys %{$data} ) {
  103         174  
185 28         37 $out{placeholder} = '';
186 28   100     63 $out{rawvalue} = $data->{$fname} || '';
187             }
188             else {
189 75 100       107 if ( $in{default} ) { $out{rawvalue} = $in{default}; }
  22         30  
190 53         67 else { $out{rawvalue} = '' }
191             }
192 103 100       150 if ( $in{placeholder} ) {
193 34         61 $out{placeholder} = qq!placeholder="$in{placeholder}"!;
194             }
195             else {
196 69         76 $out{placeholder} = '';
197             }
198              
199 103         156 $out{value} = qq!value="$out{rawvalue}"!;
200 103         507 return %out;
201             }
202              
203             sub _label {
204              
205             # an id does not get put in label because the spec does not say either
206             # the id attribute or global attributes are supported.
207             # http://www.w3.org/TR/html5/forms.html#the-label-element
208 105     105   134 my $self = shift;
209 105         107 my $field = shift;
210              
211 105 100 100     270 return '' if exists $field->{label} && !defined $field->{label};
212              
213             my $label_class
214             = $field->{label_class}
215             ? $field->{label_class}
216 104 100       162 : $self->{label_class};
217             my $label_tag
218 104 100 100     230 = exists $field->{label} ? $field->{label} || '' : ucfirst( $field->{name} );
219              
220 104         310 return $self->_build_html_tag('LABEL',
221             attributes => [
222             qq|for="$field->{id}"|,
223             qq|id="$field->{id}_label"|,
224             qq|class="$label_class"|,
225             ],
226             content => $label_tag
227             );
228             }
229              
230             sub _input {
231 73     73   6971 my $self = shift;
232 73         73 my $field = shift;
233 73         69 my $data = shift;
234 73         118 my %B = $self->_field_bits( $field, $data );
235 73         113 my $input = '';
236 73 100       96 if ( $B{textarea} ) {
237             $input = $self->_build_html_tag('TEXTAREA',
238             attributes => [
239             $B{name},
240             $B{id},
241             $B{input_class},
242             $B{placeholder},
243             $B{extra},
244             ],
245             content => $B{rawvalue}
246 2         5 );
247             }
248             else {
249             $input = $self->_build_html_tag('INPUT',
250             attributes => [
251             $B{type},
252             $B{name},
253             $B{id},
254             $B{input_class},
255             $B{placeholder},
256             $B{extra},
257             $B{value},
258 71         216 ],
259             );
260             }
261 73         172 return $input;
262             }
263              
264             sub _input_hidden {
265 12     12   34 my $self = shift;
266 12         13 my $field = shift;
267 12         13 my $data = shift;
268 12         30 my %B = $self->_field_bits( $field, $data );
269              
270             #hidden fields don't get a class or a placeholder
271             my $input = $self->_build_html_tag('INPUT',
272             attributes => [
273             qq|type="hidden"|,
274             $B{name},
275             $B{id},
276             $B{extra},
277             $B{value},
278 12         39 ],
279             );
280              
281 12         42 return $input;
282             }
283              
284             # generates the id= for option items.
285             # uses global %id_uq to insure uniqueness in generated ids.
286             # It might be cleaner to make this a sub ref under _option_input
287             # and put the hash there too, but potentially the global hash
288             # protects against a wider (though unlikely) range of collisions,
289             # also putting the code_ref in _option_id would make it that much longer.
290             sub _option_id {
291 149     149   2681 my $self = shift;
292 149         152 my $id = shift;
293 149         159 my $value = shift;
294 149         225 my $idv = $id . '_' . lc($value);
295 149         258 $idv =~ s/\s+/_/g;
296 149         265 while ( defined $id_uq{$idv} ) {
297 2         4 $id_uq{$idv}++;
298 2         6 $idv = $idv . $id_uq{$idv};
299             }
300 149         343 $id_uq{$idv} = 1;
301 149         253 return "id=\"$idv\"";
302             }
303              
304             sub _option_input { # field, input_class, data;
305 38     38   74 my $self = shift;
306 38         46 my $field = shift; # field definition from FormMap or FormHash
307 38         41 my $data = shift; # data for this form field
308 38         85 my $replace_fields = shift; # valuelist to use instead of default
309 38         69 my $datavalue = $data->{ $field->{name} };
310 38         51 my $output = '';
311 38         75 my $input_class = $self->_class_input($field);
312 38   100     102 my $extra = $field->{extra} || "";
313              
314             # in case default is 0, it must be checked in a string context
315             my $default = length( $field->{default} )
316 38 100       77 ? do {
317 19 100       21 if ( keys %{$data} ) {undef}
  19         42  
  9         15  
318 10         20 else { $field->{default} }
319             }
320             : undef;
321             my @values
322             = $replace_fields
323 5         11 ? @{$replace_fields}
324 38 100       71 : @{ $field->{values} };
  33         71  
325 38 100       76 if ( $field->{type} eq 'select' ) {
326 13         15 my @options;
327 13         20 foreach my $val (@values) {
328 34         69 my ( $value, $v_lab ) = ( split( /\:/, $val ), $val );
329 34         62 my $idf = $self->_option_id( $field->{id}, $value );
330 34         39 my $selected = '';
331 34 100       68 if ( $datavalue eq $value ) { $selected = 'selected' }
  4 100       6  
332 1         2 elsif ( $default eq $value ) { $selected = 'selected' }
333              
334 34         84 push @options, $self->_build_html_tag('option',
335             attributes => [
336             qq|value="$value"|,
337             $idf,
338             $selected,
339             ],
340             content => $v_lab,
341             prefix => ' ',
342             suffix => "\n",
343             );
344             }
345              
346 13         67 $output = $self->_build_html_tag('SELECT',
347             attributes => [
348             qq|name="$field->{name}"|,
349             qq|id="$field->{id}"|,
350             $extra,
351             $input_class
352             ],
353             # NOTE: add an empty line before first option
354             content => (join '', "\n", @options),
355             );
356             }
357             else {
358 25         32 my @options;
359 25         38 foreach my $val (@values) {
360 108         217 my ( $value, $v_lab ) = ( split( /\:/, $val ), $val );
361 108         720 my $idf = $self->_option_id( $field->{id}, $value );
362 108         118 my $checked = '';
363 108 100       213 if ( $datavalue eq $value ) {
    100          
364 5         9 $checked = q !checked="checked"!;
365             }
366             elsif ( $default eq $value ) {
367 9         11 $checked = q !checked="checked"!;
368             }
369 108         357 push @options, $self->_build_html_tag('input',
370             attributes => [
371             qq|type="$field->{type}"|,
372             $input_class,
373             $extra,
374             qq|name="$field->{name}"|,
375             $idf,
376             qq|value="$value"|,
377             $checked
378             ],
379             suffix => "$v_lab
\n"
380             );
381             }
382              
383 25         89 $output = join '', @options;
384             }
385 38         122 return $output;
386             }
387              
388             # check if $data is a hashref or a dbic result row and inflate it.
389             sub _checkdatadbic {
390 52     52   2565 my $data = shift;
391 52 100       138 if ( ref $data eq 'HASH' ) { return $data }
  16 100       40  
392 36         251 elsif ( eval { $data->isa('DBIx::Class::Row') } ) {
393 5         13 return { $data->get_inflated_columns };
394             }
395 31         79 else { return {} }
396             }
397              
398             sub generate {
399 28     28 1 24964 my $self = shift @_;
400 28         66 my $data = _checkdatadbic( shift @_ );
401 28         63 my $overide = shift @_;
402 28         43 my @generated = ();
403 28         90 $self->_clear_id_uq; # needs to be empty when form generation starts.
404 28         34 foreach my $field ( @{ $self->{FormMap} } ) {
  28         61  
405 97         124 my $input = undef;
406 97 100 100     389 if ( $field->{type} eq 'radio'
      100        
407             || $field->{type} eq 'checkbox'
408             || $field->{type} eq 'select' )
409             {
410             $input
411             = $self->_option_input( $field, $data,
412             $overide->{ $field->{name} },
413 31         103 );
414             }
415             else {
416 66         111 $input = $self->_input( $field, $data );
417             }
418             push @generated,
419             {
420             label => $self->_label($field),
421             input => $input,
422             comment => $field->{comment},
423 97         187 };
424             }
425 28         109 return \@generated;
426             }
427              
428             sub prefill {
429 4     4 1 5574 my $self = shift @_;
430 4         9 my $data = _checkdatadbic( shift @_ );
431 4         13 my $overide = shift @_;
432 4         296 my $oriFormMap = dclone $self->{FormMap};
433 4         11 foreach my $item ( @{ $self->{FormMap} } ) {
  4         13  
434 20         22 my $iname = $item->{name};
435 20 100       37 if ( $data->{$iname} ) {
436 6         13 $item->{default} = $data->{$iname};
437             }
438             }
439 4         11 my $generated = $self->generate( undef, $overide );
440 4         6 $self->{FormMap} = $oriFormMap;
441 4         25 return $generated;
442             }
443              
444             sub hidden {
445 4     4 1 1373 my $self = shift;
446 4         9 my $data = _checkdatadbic( shift @_ );
447 4         12 my $output = '';
448 4         6 foreach my $field ( @{ $self->{HiddenMap} } ) {
  4         9  
449 8         15 $output .= $self->_input_hidden( $field, $data ) . "\n";
450             }
451 4         16 return $output;
452             }
453              
454              
455             # my $data = _checkdatadbic( shift @_ );
456             # my $overide = shift @_;
457             # my @generated = ();
458              
459             sub datavalues {
460 13     13 1 12033 my $self = shift;
461 13         25 my $data = _checkdatadbic( shift @_ );
462 13         24 my $skipempty = 0;
463 13         28 my $moredata = 0;
464 13         30 for (@_) {
465 8 100       14 if ( $_ eq 'skipempty' ) { $skipempty = 1 }
  3         3  
466 8 100       15 if ( $_ eq 'moredata' ) { $moredata = 1 }
  5         6  
467             }
468 13         18 my @datavalues = ();
469             PLAINLOOP:
470 13         15 foreach my $field ( @{ $self->{FormMap} } ) {
  13         20  
471 42 100       58 if ($skipempty) {
472 12 100       22 unless ( $data->{ $field->{name} } ) { next PLAINLOOP }
  6         10  
473             }
474             my %row = (
475             name => $field->{name},
476             type => $field->{type},
477             value => $data->{ $field->{name} },
478             comment => $field->{comment},
479 36         137 );
480             $row{label}
481 36 100 50     85 = exists $field->{label} ? $field->{label} || '' : ucfirst( $field->{name} );
482             $row{id} = $field->{id}
483 36         51 ; # coverage testing deletion ? $field->{id} : 'formdiva_' . $field->{name};
484 36 100       45 if ($moredata) {
485 13         14 $row{extra} = $field->{extra};
486 13         22 $row{values} = $field->{values};
487 13         14 $row{default} = $field->{default};
488 13         15 $row{placeholder} = $field->{placeholder};
489             $row{class}
490 13 100       20 = $field->{class} ? $field->{class} : $self->{input_class};
491              
492             }
493 36         58 push @datavalues, \%row;
494             }
495 13         100 return \@datavalues;
496             }
497              
498             1;
499              
500             __END__