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.40068';
4 29     29   24182 use Moose;
  29         78  
  29         252  
5             extends 'HTML::FormHandler::Field::Compound';
6              
7 29     29   211131 use aliased 'HTML::FormHandler::Field::Repeatable::Instance';
  29         17929  
  29         190  
8 29     29   28418 use HTML::FormHandler::Field::PrimaryKey;
  29         139  
  29         1967  
9 29     29   288 use HTML::FormHandler::Merge ('merge');
  29         84  
  29         1970  
10 29     29   196 use Data::Clone ('data_clone');
  29         74  
  29         38857  
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   146 my $self = shift;
34             # loop through array of fields and validate
35 41         95 my @value_array;
36 41         1474 foreach my $field ( $self->all_fields ) {
37 80 50       381 next if ( $field->is_inactive );
38             # Validate each field and "inflate" input -> value.
39 80         475 $field->validate_field; # this calls the field's 'validate' routine
40 80 100       320 push @value_array, $field->value if $field->has_value;
41             }
42 41         366 $self->_set_value( \@value_array );
43             }
44              
45             sub init_state {
46 120     120 0 1029 my $self = shift;
47              
48             # must clear out instances built last time
49 120 100       4202 unless ( $self->has_contains ) {
50 49 100 100     1839 if ( $self->num_fields == 1 && $self->field('contains') ) {
51 16         69 $self->field('contains')->is_contains(1);
52 16         76 $self->contains( $self->field('contains') );
53             }
54             else {
55 33         254 $self->contains( $self->create_element );
56             }
57             }
58 120         4470 $self->clear_fields;
59             }
60              
61             sub create_element {
62 33     33 0 102 my ($self) = @_;
63              
64 33         82 my $instance;
65 33         227 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       1310 $instance_attr->{primary_key} = $self->primary_key
73             if $self->has_primary_key;
74 33 100       1390 if( $self->has_init_contains ) {
75 4         133 $instance_attr = merge( $self->init_contains, $instance_attr );
76             }
77 33 100       921 if( $self->form ) {
78 32         841 $instance_attr->{form} = $self->form;
79 32         849 $instance = $self->form->_make_adhoc_field(
80             'HTML::FormHandler::Field::Repeatable::Instance',
81             $instance_attr );
82             }
83             else {
84 1         51 $instance = Instance->new( %$instance_attr );
85             }
86             # copy the fields from this field into the instance
87 33         1269 $instance->add_field( $self->all_fields );
88 33         1257 foreach my $fld ( $instance->all_fields ) {
89 88         2295 $fld->parent($instance);
90             }
91              
92             # set required flag
93 33         1122 $instance->required( $self->required );
94              
95             # auto_id has no way to change widgets...deprecate this?
96 33 100       1168 if ( $self->auto_id ) {
97 4 50 33     124 unless ( grep $_->can('is_primary_key') && $_->is_primary_key, $instance->all_fields ) {
98 4         10 my $field;
99 4         18 my $field_attr = { name => 'id', parent => $instance };
100 4 50       101 if ( $self->form ) { # this will pull in the widget role
101 4         96 $field_attr->{form} = $self->form;
102 4         95 $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         136 $instance->add_field($field);
109             }
110             }
111 33         1189 $_->parent($instance) for $instance->all_fields;
112 33         1267 return $instance;
113             }
114              
115             sub clone_element {
116 195     195 0 584 my ( $self, $index ) = @_;
117              
118 195         6172 my $field = $self->contains->clone( errors => [], error_fields => [] );
119 195         771780 $field->name($index);
120 195         5420 $field->parent($self);
121 195 100       5223 if ( $field->has_fields ) {
122 142         4954 $self->clone_fields( $field, [ $field->all_fields ] );
123             }
124 195         751 return $field;
125             }
126              
127             sub clone_fields {
128 152     152 0 496 my ( $self, $parent, $fields ) = @_;
129 152         357 my @field_array;
130 152         4676 $parent->fields( [] );
131 152         344 foreach my $field ( @{$fields} ) {
  152         462  
132 462         2362 my $new_field = $field->clone( errors => [], error_fields => [] );
133 462 100       1684359 if ( $new_field->has_fields ) {
134 10         406 $self->clone_fields( $new_field, [ $new_field->all_fields ] );
135             }
136 462         17925 $new_field->parent($parent);
137 462         15557 $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   152 my ( $self, $result, $input ) = @_;
144              
145 42         419 $self->init_state;
146 42         1324 $result->_set_input($input);
147 42         1181 $self->_set_result($result);
148             # if Repeatable has array input, need to build instances
149 42         1284 $self->fields( [] );
150 42         108 my $index = 0;
151 42 100       172 if ( ref $input eq 'ARRAY' ) {
152             # build appropriate instance array
153 39         90 foreach my $element ( @{$input} ) {
  39         121  
154 84 100       265 next if not defined $element; # skip empty slots
155 82         336 my $field = $self->clone_element($index);
156 82         2284 my $result = HTML::FormHandler::Field::Result->new(
157             name => $index,
158             parent => $self->result
159             );
160 82         726 $result = $field->_result_from_input( $result, $element, 1 );
161 82         2225 $self->result->add_result($result);
162 82         2775 $self->add_field($field);
163 82         224 $index++;
164             }
165             }
166 42         1322 $self->index($index);
167 42 50       1282 $self->_setup_for_js if $self->setup_for_js;
168 42         1068 $self->result->_set_field_def($self);
169 42         1049 return $self->result;
170             }
171              
172             sub _setup_for_js {
173 1     1   2 my $self = shift;
174 1 50       27 return unless $self->form;
175 1         5 my $full_name = $self->full_name;
176 1         4 my $index_level =()= $full_name =~ /{index\d+}/g;
177 1         2 $index_level++;
178 1         4 my $field_name = "{index-$index_level}";
179 1         7 my $field = $self->_add_extra($field_name);
180 1         8 my $rendered = $field->render;
181             # remove extra result & field, now that it's rendered
182 1         29 $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         30 $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   49 my ( $self, $result, $values ) = @_;
193              
194 16 50 33     493 return $self->_result_from_fields($result)
195             if ( $self->num_when_empty > 0 && !$values );
196 16         478 $self->item($values);
197 16         86 $self->init_state;
198 16         757 $self->_set_result($result);
199             # Create field instances and fill with values
200 16         36 my $index = 0;
201 16         34 my @new_values;
202 16         453 $self->fields( [] );
203 16 50 33     111 $values = [$values] if ( $values && ref $values ne 'ARRAY' );
204 16         42 foreach my $element ( @{$values} ) {
  16         51  
205 35 50       118 next unless $element;
206 35         145 my $field = $self->clone_element($index);
207 35         978 my $result =
208             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
209 35 100       1268 if( $field->has_inflate_default_method ) {
210 2         73 $element = $field->inflate_default($element);
211             }
212 35         255 $result = $field->_result_from_object( $result, $element );
213 35         949 push @new_values, $result->value;
214 35         1171 $self->add_field($field);
215 35         895 $self->result->add_result( $field->result );
216 35         110 $index++;
217             }
218 16 100       562 if( my $num_extra = $self->num_extra ) {
219 1         4 while ($num_extra ) {
220 1         10 $self->_add_extra($index);
221 1         3 $num_extra--;
222 1         4 $index++;
223             }
224             }
225 16         466 $self->index($index);
226 16 50       480 $self->_setup_for_js if $self->setup_for_js;
227 16 100       74 $values = \@new_values if scalar @new_values;
228 16         111 $self->_set_value($values);
229 16         387 $self->result->_set_field_def($self);
230 16         381 return $self->result;
231             }
232              
233             sub _add_extra {
234 3     3   12 my ($self, $index) = @_;
235              
236 3         21 my $field = $self->clone_element($index);
237 3         91 my $result =
238             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
239 3         23 $result = $field->_result_from_fields($result);
240 3 50       89 $self->result->add_result($result) if $result;
241 3         96 $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       6 $count = 1 if not defined $count;
248 1         29 my $index = $self->index;
249 1         3 while ( $count ) {
250 1         7 $self->_add_extra($index);
251 1         3 $count--;
252 1         4 $index++;
253             }
254 1         36 $self->index($index);
255             }
256              
257             # create an empty field
258             sub _result_from_fields {
259 62     62   213 my ( $self, $result ) = @_;
260              
261             # check for defaults
262 62 100       393 if ( my @values = $self->get_default_value ) {
263 2         37 return $self->_result_from_object( $result, \@values );
264             }
265 60         431 $self->init_state;
266 60         2008 $self->_set_result($result);
267 60         2061 my $count = $self->num_when_empty;
268 60         149 my $index = 0;
269             # build empty instance
270 60         1926 $self->fields( [] );
271 60         238 while ( $count > 0 ) {
272 75         407 my $field = $self->clone_element($index);
273 75         2470 my $result =
274             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
275 75         763 $result = $field->_result_from_fields($result);
276 75 50       2207 $self->result->add_result($result) if $result;
277 75         2637 $self->add_field($field);
278 75         165 $index++;
279 75         315 $count--;
280             }
281 60         2155 $self->index($index);
282 60 100       2006 $self->_setup_for_js if $self->setup_for_js;
283 60         1606 $self->result->_set_field_def($self);
284 60         243 return $result;
285             }
286              
287             __PACKAGE__->meta->make_immutable;
288 29     29   294 use namespace::autoclean;
  29         86  
  29         283  
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.40068
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) 2017 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