File Coverage

blib/lib/Data/MuForm/Field/Repeatable.pm
Criterion Covered Total %
statement 150 160 93.7
branch 36 48 75.0
condition 4 9 44.4
subroutine 20 21 95.2
pod 1 12 8.3
total 211 250 84.4


line stmt bran cond sub pod time code
1             package Data::MuForm::Field::Repeatable;
2             # ABSTRACT: repeatable (array) field
3              
4 21     21   15171 use Moo;
  21         27  
  21         120  
5 21     21   5526 use Data::MuForm::Meta;
  21         35  
  21         133  
6             extends 'Data::MuForm::Field::Compound';
7              
8 21     21   16003 use aliased 'Data::MuForm::Field::Repeatable::Instance';
  21         10945  
  21         242  
9 21     21   10620 use Data::MuForm::Field::PrimaryKey;
  21         46  
  21         589  
10 21     21   109 use Data::MuForm::Merge ('merge');
  21         29  
  21         1151  
11 21     21   82 use Data::Clone ('data_clone');
  21         29  
  21         760  
12 21     21   73 use Types::Standard -types;
  21         23  
  21         96  
13              
14              
15             has 'contains' => (
16             is => 'rw',
17             predicate => 'has_contains',
18             );
19              
20             has 'init_instance' => ( is => 'rw', isa => HashRef, default => sub {{}},
21             );
22 24 50   24 0 87 sub has_init_instance { exists $_[0]->{init_instance} && scalar keys %{$_[0]->{init_instance}} }
  24         93  
23              
24             has 'num_when_empty' => ( is => 'rw', default => 1 );
25             has 'num_extra' => ( is => 'rw', default => 0 );
26             has 'setup_for_js' => ( is => 'rw' );
27             has 'index' => ( is => 'rw', default => 0 );
28 63     63 0 811 sub is_repeatable {1}
29             #has '+widget' => ( default => 'Repeatable' );
30              
31             sub fields_validate {
32 26     26 0 33 my $self = shift;
33             # loop through array of fields and validate
34 26         29 my @value_array;
35 26         69 foreach my $field ( $self->all_fields ) {
36 53 50       155 next if ( ! $field->active );
37             # Validate each field and "inflate" input -> value.
38 53         158 $field->field_validate; # this calls the field's 'validate' routine
39 53 100       382 push @value_array, $field->value if $field->has_value;
40             }
41 26         136 $self->value( \@value_array );
42             }
43              
44             sub init_state {
45 85     85 0 105 my $self = shift;
46              
47             # must clear out instances built last time
48 85 100       238 unless ( $self->has_contains ) {
49 36 100 66     153 if ( $self->num_fields == 1 && $self->field('contains') ) {
50 12         31 $self->field('contains')->is_contains(1);
51 12         50 $self->contains( $self->field('contains') );
52             }
53             else {
54 24         63 $self->contains( $self->create_element );
55             }
56             }
57 85         322 $self->clear_fields;
58             }
59              
60             sub create_element {
61 24     24 0 36 my ($self) = @_;
62              
63 24         24 my $instance;
64 24         489 my $instance_attr = {
65             name => 'contains',
66             parent => $self,
67             type => 'Repeatable::Instance',
68             is_contains => 1,
69             localizer => $self->localizer,
70             renderer => $self->renderer,
71             };
72             # primary_key array is used for reloading after database update
73 24 100       807 $instance_attr->{primary_key} = $self->primary_key
74             if $self->has_primary_key;
75 24 100       98 if( $self->has_init_instance ) {
76 1         14 $instance_attr = merge( $self->init_instance, $instance_attr );
77             }
78 24 50       380 if( $self->form ) {
79 24         403 $instance_attr->{form} = $self->form;
80 24         373 $instance = $self->form->_make_adhoc_field(
81             'Data::MuForm::Field::Repeatable::Instance',
82             $instance_attr );
83             }
84             else {
85 0         0 $instance = Instance->new( %$instance_attr );
86             }
87             # copy the fields from this field into the instance
88 24         100 $instance->push_field( $self->all_fields );
89 24         111 foreach my $fld ( $instance->all_fields ) {
90 72         1250 $fld->parent($instance);
91             }
92              
93             # set required flag
94 24         283 $instance->required( $self->required );
95              
96 24         60 $_->parent($instance) for $instance->all_fields;
97 24         986 return $instance;
98             }
99              
100             sub clone_element {
101 130     130 0 137 my ( $self, $index ) = @_;
102              
103 130         654 my $field = data_clone($self->contains);
104 130         560 $field->clear_errors;
105 130         410 $field->clear_error_fields;
106 130         298 $field->name($index);
107 130         2439 $field->parent($self);
108 130 100       1105 if ( $field->has_fields ) {
109 98         249 $self->clone_fields( $field, [ $field->all_fields ] );
110             }
111 130         1598 return $field;
112             }
113              
114             sub clone_fields {
115 104     104 0 119 my ( $self, $parent, $fields ) = @_;
116              
117 104         93 my @field_array;
118 104         315 $parent->fields( [] );
119 104         3067 foreach my $field ( @{$fields} ) {
  104         180  
120 317         875 my $new_field = data_clone($field);
121 317         1468 $new_field->clear_errors;
122 317         725 $new_field->clear_error_fields;
123 317 100       665 if ( $new_field->has_fields ) {
124 6         16 $self->clone_fields( $new_field, [ $new_field->all_fields ] );
125             }
126 317         6000 $new_field->parent($parent);
127 317         2648 $parent->push_field($new_field);
128             }
129             }
130              
131             # params exist and validation will be performed (later)
132             sub fill_from_params {
133 29     29 0 40 my ( $self, $input ) = @_;
134              
135 29         65 $self->init_state;
136 29         1025 $self->input($input);
137             # if Repeatable has array input, need to build instances
138 29         104 $self->fields( [] );
139 29         1032 my $index = 0;
140 29 100       83 if ( ref $input eq 'ARRAY' ) {
141             # build appropriate instance array
142 27         27 foreach my $element ( @{$input} ) {
  27         51  
143 56 100       103 next if not defined $element; # skip empty slots
144 55         98 my $field = $self->clone_element($index);
145 55         736 $field->fill_from_params( $element, 1 );
146 55         156 $self->push_field($field);
147 55         82 $index++;
148             }
149             }
150 29         85 $self->index($index);
151 29 50       79 $self->_setup_for_js if $self->setup_for_js;
152 29         53 return;
153             }
154              
155             sub _setup_for_js {
156 0     0   0 my $self = shift;
157 0 0       0 return unless $self->form;
158 0         0 my $full_name = $self->full_name;
159 0         0 my $index_level =()= $full_name =~ /{index\d+}/g;
160 0         0 $index_level++;
161 0         0 my $field_name = "{index-$index_level}";
162 0         0 my $field = $self->_add_extra($field_name);
163 0         0 my $rendered = $field->render;
164             # remove extra result & field, now that it's rendered
165             # $self->result->_pop_result;
166             # $self->_pop_field;
167             # set the information in the form
168             # $self->index is the index of the next instance
169 0         0 $self->form->set_for_js( $self->full_name,
170             { index => $self->index, html => $rendered, level => $index_level } );
171             }
172              
173             # this is called when there is an init_values or a db model with values
174             sub fill_from_object {
175 15     15 0 18 my ( $self, $values ) = @_;
176              
177 15 50 33     119 return $self->fill_from_fields()
178             if ( $self->num_when_empty > 0 && !$values );
179 15         68 $self->obj($values);
180 15         30 $self->init_state;
181             # Create field instances and fill with values
182 15         581 my $index = 0;
183 15         19 my @new_values;
184 15         58 $self->fields( [] );
185 15 50 33     982 $values = [$values] if ( $values && ref $values ne 'ARRAY' );
186 15         20 foreach my $element ( @{$values} ) {
  15         32  
187 32 50       58 next unless $element;
188 32         61 my $field = $self->clone_element($index);
189 32 100       128 if( $field->has_transform_default_to_value ) {
190 2         8 $element = $field->transform_default_to_value->($field, $element);
191             }
192 32         510 $field->fill_from_object( $element );
193 32         61 push @new_values, $field->value;
194 32         88 $self->push_field($field);
195 32         328 $index++;
196             }
197 15 100       62 if( my $num_extra = $self->num_extra ) {
198 1         3 while ($num_extra ) {
199 1         2 $self->_add_extra($index);
200 1         2 $num_extra--;
201 1         3 $index++;
202             }
203             }
204 15         39 $self->index($index);
205 15 50       40 $self->_setup_for_js if $self->setup_for_js;
206 15 100       39 $values = \@new_values if scalar @new_values;
207 15         42 $self->value($values);
208 15         33 return;
209             }
210              
211             sub _add_extra {
212 2     2   2 my ($self, $index) = @_;
213              
214 2         4 my $field = $self->clone_element($index);
215 2         7 $field->fill_from_fields();
216 2         5 $self->push_field($field);
217 2         1 return $field;
218             }
219              
220             sub add_extra {
221 1     1 1 6 my ( $self, $count ) = @_;
222 1 50       4 $count = 1 if not defined $count;
223 1         3 my $index = $self->index;
224 1         3 while ( $count ) {
225 1         3 $self->_add_extra($index);
226 1         2 $count--;
227 1         2 $index++;
228             }
229 1         3 $self->index($index);
230             }
231              
232             # create an empty field
233             sub fill_from_fields {
234 42     42 0 55 my ( $self, ) = @_;
235              
236             # check for defaults
237 42 100       169 if ( my @values = $self->get_default_value ) {
238 2         33 return $self->fill_from_object( \@values );
239             }
240 40         107 $self->init_state;
241 40         315 my $count = $self->num_when_empty;
242 40         47 my $index = 0;
243             # build empty instance
244 40         188 $self->fields( [] );
245 40         8674 while ( $count > 0 ) {
246 41         119 my $field = $self->clone_element($index);
247 41         197 $field->fill_from_fields();
248 41         124 $self->push_field($field);
249 41         44 $index++;
250 41         103 $count--;
251             }
252 40         96 $self->index($index);
253 40 50       122 $self->_setup_for_js if $self->setup_for_js;
254 40         62 return;
255             }
256              
257             sub render {
258 4     4 0 24 my ( $self, $rargs ) = @_;
259 4         31 my $render_args = $self->get_render_args(%$rargs);
260 4         93 return $self->renderer->render_repeatable($render_args, $self->fields);
261             }
262              
263              
264             1;
265              
266             __END__
267              
268             =pod
269              
270             =encoding UTF-8
271              
272             =head1 NAME
273              
274             Data::MuForm::Field::Repeatable - repeatable (array) field
275              
276             =head1 VERSION
277              
278             version 0.04
279              
280             =head1 SYNOPSIS
281              
282             In a form, for an array of hashrefs, equivalent to a 'has_many' database
283             relationship.
284              
285             has_field 'addresses' => ( type => 'Repeatable' );
286             has_field 'addresses.address_id' => ( type => 'PrimaryKey' );
287             has_field 'addresses.street';
288             has_field 'addresses.city';
289             has_field 'addresses.state';
290              
291             In a form, for an array of single fields (not directly equivalent to a
292             database relationship) use the 'contains' pseudo field name:
293              
294             has_field 'tags' => ( type => 'Repeatable' );
295             has_field 'tags.contains' => ( type => 'Text',
296             apply => [ { check => ['perl', 'programming', 'linux', 'internet'],
297             message => 'Not a valid tag' } ]
298             );
299              
300             or use 'contains' with single fields which are compound fields:
301              
302             has_field 'addresses' => ( type => 'Repeatable' );
303             has_field 'addresses.contains' => ( type => '+MyAddress' );
304              
305             If the MyAddress field contains fields 'address_id', 'street', 'city', and
306             'state', then this syntax is functionally equivalent to the first method
307             where the fields are declared with dots ('addresses.city');
308              
309             You can pass attributes to the 'contains' field by supplying an 'init_instance' hashref.
310              
311             has_field 'addresses' => ( type => 'Repeatable,
312             init_instance => { wrapper_attr => { class => ['hfh', 'repinst'] } },
313             );
314              
315             =head1 DESCRIPTION
316              
317             This class represents an array. It can either be an array of hashrefs
318             (compound fields) or an array of single fields.
319              
320             The 'contains' keyword is used for elements that do not have names
321             because they are not hash elements.
322              
323             This field node will build arrays of fields from the parameters or an
324             initial object, or empty fields for an empty form.
325              
326             The name of the element fields will be an array index,
327             starting with 0. Therefore the first array element can be accessed with:
328              
329             $form->field('tags')->field('0')
330             $form->field('addresses')->field('0')->field('city')
331              
332             or using the shortcut form:
333              
334             $form->field('tags.0')
335             $form->field('addresses.0.city')
336              
337             The array of elements will be in C<< $form->field('addresses')->fields >>.
338             The subfields of the elements will be in a fields array in each element.
339              
340             foreach my $element ( $form->field('addresses')->fields )
341             {
342             foreach my $field ( $element->fields )
343             {
344             # do something
345             }
346             }
347              
348             Every field that has a 'fields' array will also have an 'error_fields' array
349             containing references to the fields that contain errors.
350              
351             =head2 Complications
352              
353             When new elements are created by a Repeatable field in a database form
354             an attempt is made to re-load the Repeatable field from the database, because
355             otherwise the repeatable elements will not have primary keys. Although this
356             works, if you have included other fields in your repeatable elements
357             that do *not* come from the database, the defaults/values must be
358             able to be loaded in a way that works when the form is initialized from
359             the database model (row). This is only an issue if you re-present the form
360             after the database update succeeds.
361              
362             =head1 ATTRIBUTES
363              
364             =over
365              
366             =item index
367              
368             This attribute contains the next index number available to create an
369             additional array element.
370              
371             =item num_when_empty
372              
373             This attribute (default 1) indicates how many empty fields to present
374             in an empty form which hasn't been filled from parameters or database
375             rows.
376              
377             =item num_extra
378              
379             When the field results are built from an existing object (model or init_values)
380             an additional number of repeatable elements will be created equal to this
381             number. Default is 0.
382              
383             =item add_extra
384              
385             When a form is submitted and the field results are built from the input
386             parameters, it's not clear when or if an additional repeatable element might
387             be wanted. The method 'add_extra' will add an empty repeatable element.
388              
389             $form->process( params => {....} );
390             $form->field('my_repeatable')->add_extra(1);
391              
392             This might be useful if the form is being re-presented to the user.
393              
394             =back
395              
396             =head1 AUTHOR
397              
398             Gerda Shank
399              
400             =head1 COPYRIGHT AND LICENSE
401              
402             This software is copyright (c) 2017 by Gerda Shank.
403              
404             This is free software; you can redistribute it and/or modify it under
405             the same terms as the Perl 5 programming language system itself.
406              
407             =cut