File Coverage

blib/lib/HTML/FormHandler/Field/Repeatable.pm
Criterion Covered Total %
statement 171 172 99.4
branch 44 56 78.5
condition 6 12 50.0
subroutine 17 17 100.0
pod 1 5 20.0
total 239 262 91.2


line stmt bran cond sub pod time code
1             package HTML::FormHandler::Field::Repeatable;
2             # ABSTRACT: repeatable (array) field
3             $HTML::FormHandler::Field::Repeatable::VERSION = '0.40067';
4 29     29   21614 use Moose;
  29         46  
  29         202  
5             extends 'HTML::FormHandler::Field::Compound';
6              
7 29     29   146425 use aliased 'HTML::FormHandler::Field::Repeatable::Instance';
  29         16736  
  29         171  
8 29     29   18517 use HTML::FormHandler::Field::PrimaryKey;
  29         90  
  29         1261  
9 29     29   202 use HTML::FormHandler::Merge ('merge');
  29         43  
  29         1828  
10 29     29   129 use Data::Clone ('data_clone');
  29         41  
  29         32742  
11              
12              
13             has 'contains' => (
14             isa => 'HTML::FormHandler::Field',
15             is => 'rw',
16             predicate => 'has_contains',
17             );
18              
19             has 'init_contains' => ( is => 'rw', isa => 'HashRef', traits => ['Hash'],
20             default => sub {{}},
21             handles => { has_init_contains => 'count' },
22             );
23              
24             has 'num_when_empty' => ( isa => 'Int', is => 'rw', default => 1 );
25             has 'num_extra' => ( isa => 'Int', is => 'rw', default => 0 );
26             has 'setup_for_js' => ( isa => 'Bool', is => 'rw' );
27             has 'index' => ( isa => 'Int', is => 'rw', default => 0 );
28             has 'auto_id' => ( isa => 'Bool', is => 'rw', default => 0 );
29             has 'is_repeatable' => ( isa => 'Bool', is => 'ro', default => 1 );
30             has '+widget' => ( default => 'Repeatable' );
31              
32             sub _fields_validate {
33 41     41   70 my $self = shift;
34             # loop through array of fields and validate
35 41         66 my @value_array;
36 41         1462 foreach my $field ( $self->all_fields ) {
37 80 50       287 next if ( $field->is_inactive );
38             # Validate each field and "inflate" input -> value.
39 80         370 $field->validate_field; # this calls the field's 'validate' routine
40 80 100       218 push @value_array, $field->value if $field->has_value;
41             }
42 41         268 $self->_set_value( \@value_array );
43             }
44              
45             sub init_state {
46 120     120 0 381 my $self = shift;
47              
48             # must clear out instances built last time
49 120 100       3858 unless ( $self->has_contains ) {
50 49 100 100     1678 if ( $self->num_fields == 1 && $self->field('contains') ) {
51 16         48 $self->field('contains')->is_contains(1);
52 16         50 $self->contains( $self->field('contains') );
53             }
54             else {
55 33         200 $self->contains( $self->create_element );
56             }
57             }
58 120         3968 $self->clear_fields;
59             }
60              
61             sub create_element {
62 33     33 0 61 my ($self) = @_;
63              
64 33         47 my $instance;
65 33         176 my $instance_attr = {
66             name => 'contains',
67             parent => $self,
68             type => 'Repeatable::Instance',
69             is_contains => 1,
70             };
71             # primary_key array is used for reloading after database update
72 33 100       1169 $instance_attr->{primary_key} = $self->primary_key
73             if $self->has_primary_key;
74 33 100       1312 if( $self->has_init_contains ) {
75 4         114 $instance_attr = merge( $self->init_contains, $instance_attr );
76             }
77 33 100       875 if( $self->form ) {
78 32         758 $instance_attr->{form} = $self->form;
79 32         785 $instance = $self->form->_make_adhoc_field(
80             'HTML::FormHandler::Field::Repeatable::Instance',
81             $instance_attr );
82             }
83             else {
84 1         39 $instance = Instance->new( %$instance_attr );
85             }
86             # copy the fields from this field into the instance
87 33         1094 $instance->add_field( $self->all_fields );
88 33         1113 foreach my $fld ( $instance->all_fields ) {
89 88         1989 $fld->parent($instance);
90             }
91              
92             # set required flag
93 33         967 $instance->required( $self->required );
94              
95             # auto_id has no way to change widgets...deprecate this?
96 33 100       1024 if ( $self->auto_id ) {
97 4 50 33     119 unless ( grep $_->can('is_primary_key') && $_->is_primary_key, $instance->all_fields ) {
98 4         6 my $field;
99 4         16 my $field_attr = { name => 'id', parent => $instance };
100 4 50       98 if ( $self->form ) { # this will pull in the widget role
101 4         93 $field_attr->{form} = $self->form;
102 4         89 $field = $self->form->_make_adhoc_field(
103             'HTML::FormHandler::Field::PrimaryKey', $field_attr );
104             }
105             else { # the following won't have a widget role applied
106 0         0 $field = HTML::FormHandler::Field::PrimaryKey->new( %$field_attr );
107             }
108 4         128 $instance->add_field($field);
109             }
110             }
111 33         1030 $_->parent($instance) for $instance->all_fields;
112 33         1054 return $instance;
113             }
114              
115             sub clone_element {
116 195     195 0 278 my ( $self, $index ) = @_;
117              
118 195         5447 my $field = $self->contains->clone( errors => [], error_fields => [] );
119 195         392647 $field->name($index);
120 195         4813 $field->parent($self);
121 195 100       4676 if ( $field->has_fields ) {
122 142         4421 $self->clone_fields( $field, [ $field->all_fields ] );
123             }
124 195         441 return $field;
125             }
126              
127             sub clone_fields {
128 152     152 0 245 my ( $self, $parent, $fields ) = @_;
129 152         186 my @field_array;
130 152         4083 $parent->fields( [] );
131 152         169 foreach my $field ( @{$fields} ) {
  152         303  
132 462         1549 my $new_field = $field->clone( errors => [], error_fields => [] );
133 462 100       830247 if ( $new_field->has_fields ) {
134 10         349 $self->clone_fields( $new_field, [ $new_field->all_fields ] );
135             }
136 462         14306 $new_field->parent($parent);
137 462         14272 $parent->add_field($new_field);
138             }
139             }
140              
141             # params exist and validation will be performed (later)
142             sub _result_from_input {
143 42     42   83 my ( $self, $result, $input ) = @_;
144              
145 42         155 $self->init_state;
146 42         1305 $result->_set_input($input);
147 42         1133 $self->_set_result($result);
148             # if Repeatable has array input, need to build instances
149 42         1143 $self->fields( [] );
150 42         58 my $index = 0;
151 42 100       132 if ( ref $input eq 'ARRAY' ) {
152             # build appropriate instance array
153 39         64 foreach my $element ( @{$input} ) {
  39         98  
154 84 100       178 next if not defined $element; # skip empty slots
155 82         209 my $field = $self->clone_element($index);
156 82         2038 my $result = HTML::FormHandler::Field::Result->new(
157             name => $index,
158             parent => $self->result
159             );
160 82         397 $result = $field->_result_from_input( $result, $element, 1 );
161 82         1955 $self->result->add_result($result);
162 82         2554 $self->add_field($field);
163 82         141 $index++;
164             }
165             }
166 42         1200 $self->index($index);
167 42 50       1218 $self->_setup_for_js if $self->setup_for_js;
168 42         986 $self->result->_set_field_def($self);
169 42         965 return $self->result;
170             }
171              
172             sub _setup_for_js {
173 1     1   3 my $self = shift;
174 1 50       26 return unless $self->form;
175 1         5 my $full_name = $self->full_name;
176 1         3 my $index_level =()= $full_name =~ /{index\d+}/g;
177 1         2 $index_level++;
178 1         4 my $field_name = "{index-$index_level}";
179 1         6 my $field = $self->_add_extra($field_name);
180 1         7 my $rendered = $field->render;
181             # remove extra result & field, now that it's rendered
182 1         25 $self->result->_pop_result;
183 1         42 $self->_pop_field;
184             # set the information in the form
185             # $self->index is the index of the next instance
186 1         24 $self->form->set_for_js( $self->full_name,
187             { index => $self->index, html => $rendered, level => $index_level } );
188             }
189              
190             # this is called when there is an init_object or a db item with values
191             sub _result_from_object {
192 16     16   30 my ( $self, $result, $values ) = @_;
193              
194 16 50 33     467 return $self->_result_from_fields($result)
195             if ( $self->num_when_empty > 0 && !$values );
196 16         452 $self->item($values);
197 16         60 $self->init_state;
198 16         429 $self->_set_result($result);
199             # Create field instances and fill with values
200 16         23 my $index = 0;
201 16         24 my @new_values;
202 16         427 $self->fields( [] );
203 16 50 33     89 $values = [$values] if ( $values && ref $values ne 'ARRAY' );
204 16         23 foreach my $element ( @{$values} ) {
  16         41  
205 35 50       69 next unless $element;
206 35         91 my $field = $self->clone_element($index);
207 35         875 my $result =
208             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
209 35 100       1168 if( $field->has_inflate_default_method ) {
210 2         54 $element = $field->inflate_default($element);
211             }
212 35         166 $result = $field->_result_from_object( $result, $element );
213 35         900 push @new_values, $result->value;
214 35         1063 $self->add_field($field);
215 35         809 $self->result->add_result( $field->result );
216 35         65 $index++;
217             }
218 16 100       549 if( my $num_extra = $self->num_extra ) {
219 1         3 while ($num_extra ) {
220 1         12 $self->_add_extra($index);
221 1         2 $num_extra--;
222 1         4 $index++;
223             }
224             }
225 16         485 $self->index($index);
226 16 50       490 $self->_setup_for_js if $self->setup_for_js;
227 16 100       56 $values = \@new_values if scalar @new_values;
228 16         81 $self->_set_value($values);
229 16         349 $self->result->_set_field_def($self);
230 16         388 return $self->result;
231             }
232              
233             sub _add_extra {
234 3     3   6 my ($self, $index) = @_;
235              
236 3         10 my $field = $self->clone_element($index);
237 3         76 my $result =
238             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
239 3         16 $result = $field->_result_from_fields($result);
240 3 50       79 $self->result->add_result($result) if $result;
241 3         92 $self->add_field($field);
242 3         6 return $field;
243             }
244              
245             sub add_extra {
246 1     1 1 3 my ( $self, $count ) = @_;
247 1 50       5 $count = 1 if not defined $count;
248 1         41 my $index = $self->index;
249 1         3 while ( $count ) {
250 1         12 $self->_add_extra($index);
251 1         2 $count--;
252 1         3 $index++;
253             }
254 1         27 $self->index($index);
255             }
256              
257             # create an empty field
258             sub _result_from_fields {
259 62     62   126 my ( $self, $result ) = @_;
260              
261             # check for defaults
262 62 100       297 if ( my @values = $self->get_default_value ) {
263 2         35 return $self->_result_from_object( $result, \@values );
264             }
265 60         296 $self->init_state;
266 60         1706 $self->_set_result($result);
267 60         1783 my $count = $self->num_when_empty;
268 60         80 my $index = 0;
269             # build empty instance
270 60         1642 $self->fields( [] );
271 60         158 while ( $count > 0 ) {
272 75         302 my $field = $self->clone_element($index);
273 75         2118 my $result =
274             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
275 75         509 $result = $field->_result_from_fields($result);
276 75 50       1877 $self->result->add_result($result) if $result;
277 75         2318 $self->add_field($field);
278 75         89 $index++;
279 75         205 $count--;
280             }
281 60         1827 $self->index($index);
282 60 100       1804 $self->_setup_for_js if $self->setup_for_js;
283 60         1385 $self->result->_set_field_def($self);
284 60         157 return $result;
285             }
286              
287             __PACKAGE__->meta->make_immutable;
288 29     29   169 use namespace::autoclean;
  29         47  
  29         214  
289             1;
290              
291             __END__
292              
293             =pod
294              
295             =encoding UTF-8
296              
297             =head1 NAME
298              
299             HTML::FormHandler::Field::Repeatable - repeatable (array) field
300              
301             =head1 VERSION
302              
303             version 0.40067
304              
305             =head1 SYNOPSIS
306              
307             In a form, for an array of hashrefs, equivalent to a 'has_many' database
308             relationship.
309              
310             has_field 'addresses' => ( type => 'Repeatable' );
311             has_field 'addresses.address_id' => ( type => 'PrimaryKey' );
312             has_field 'addresses.street';
313             has_field 'addresses.city';
314             has_field 'addresses.state';
315              
316             In a form, for an array of single fields (not directly equivalent to a
317             database relationship) use the 'contains' pseudo field name:
318              
319             has_field 'tags' => ( type => 'Repeatable' );
320             has_field 'tags.contains' => ( type => 'Text',
321             apply => [ { check => ['perl', 'programming', 'linux', 'internet'],
322             message => 'Not a valid tag' } ]
323             );
324              
325             or use 'contains' with single fields which are compound fields:
326              
327             has_field 'addresses' => ( type => 'Repeatable' );
328             has_field 'addresses.contains' => ( type => '+MyAddress' );
329              
330             If the MyAddress field contains fields 'address_id', 'street', 'city', and
331             'state', then this syntax is functionally equivalent to the first method
332             where the fields are declared with dots ('addresses.city');
333              
334             You can pass attributes to the 'contains' field by supplying an 'init_contains' hashref.
335              
336             has_field 'addresses' => ( type => 'Repeatable,
337             init_contains => { wrapper_attr => { class => ['hfh', 'repinst'] } },
338             );
339              
340             =head1 DESCRIPTION
341              
342             This class represents an array. It can either be an array of hashrefs
343             (compound fields) or an array of single fields.
344              
345             The 'contains' keyword is used for elements that do not have names
346             because they are not hash elements.
347              
348             This field node will build arrays of fields from the parameters or an
349             initial object, or empty fields for an empty form.
350              
351             The name of the element fields will be an array index,
352             starting with 0. Therefore the first array element can be accessed with:
353              
354             $form->field('tags')->field('0')
355             $form->field('addresses')->field('0')->field('city')
356              
357             or using the shortcut form:
358              
359             $form->field('tags.0')
360             $form->field('addresses.0.city')
361              
362             The array of elements will be in C<< $form->field('addresses')->fields >>.
363             The subfields of the elements will be in a fields array in each element.
364              
365             foreach my $element ( $form->field('addresses')->fields )
366             {
367             foreach my $field ( $element->fields )
368             {
369             # do something
370             }
371             }
372              
373             Every field that has a 'fields' array will also have an 'error_fields' array
374             containing references to the fields that contain errors.
375              
376             =head2 Complications
377              
378             When new elements are created by a Repeatable field in a database form
379             an attempt is made to re-load the Repeatable field from the database, because
380             otherwise the repeatable elements will not have primary keys. Although this
381             works, if you have included other fields in your repeatable elements
382             that do *not* come from the database, the defaults/values must be
383             able to be loaded in a way that works when the form is initialized from
384             the database item. This is only an issue if you re-present the form
385             after the database update succeeds.
386              
387             =head1 ATTRIBUTES
388              
389             =over
390              
391             =item index
392              
393             This attribute contains the next index number available to create an
394             additional array element.
395              
396             =item init_contains
397              
398             When the Repeatable is repeated, this hashref will be used to initialise each
399             instance. It can contain any of the same attributes you would normally give to
400             L<HTML::FormHandler/has_field>, except C<type>, for obvious reasons.
401              
402             has_field 'addresses' => (
403             type => 'Repeatable',
404             init_contains => {
405             wrapper_attr => {
406             rel => 'address'
407             },
408             wrapper_class => [ 'row' ]
409             }
410             );
411              
412             Note that providing a C<wrapper_class> to this hashref will override the default
413             C<hfh-repinst> class on these instances.
414              
415             =item num_when_empty
416              
417             This attribute (default 1) indicates how many empty fields to present
418             in an empty form which hasn't been filled from parameters or database
419             rows.
420              
421             =item num_extra
422              
423             When the field results are built from an existing object (item or init_object)
424             an additional number of repeatable elements will be created equal to this
425             number. Default is 0.
426              
427             =item add_extra
428              
429             When a form is submitted and the field results are built from the input
430             parameters, it's not clear when or if an additional repeatable element might
431             be wanted. The method 'add_extra' will add an empty repeatable element.
432              
433             $form->process( params => {....} );
434             $form->field('my_repeatable')->add_extra(1);
435              
436             This might be useful if the form is being re-presented to the user.
437              
438             =item setup_for_js
439              
440             setup_for_js => 1
441              
442             Saves information in the form for javascript to use when adding repeatable elements.
443             If using the example javascript, you also must set 'do_wrapper' in the
444             Repeatable field and use the Bootstrap widget wrapper (or wrap the repeatable
445             elements in a 'controls' div by setting tags => { controls_div => 1 }.
446             See t/repeatable/js.t for an example. See also
447             L<HTML::FormHandler::Render::RepeatableJs> and L<HTML::FormHandler::Field::AddElement>.
448              
449             =back
450              
451             =head1 AUTHOR
452              
453             FormHandler Contributors - see HTML::FormHandler
454              
455             =head1 COPYRIGHT AND LICENSE
456              
457             This software is copyright (c) 2016 by Gerda Shank.
458              
459             This is free software; you can redistribute it and/or modify it under
460             the same terms as the Perl 5 programming language system itself.
461              
462             =cut