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   15215 use Moo;
  21         31  
  21         125  
5 21     21   5820 use Data::MuForm::Meta;
  21         27  
  21         143  
6             extends 'Data::MuForm::Field::Compound';
7              
8 21     21   14748 use aliased 'Data::MuForm::Field::Repeatable::Instance';
  21         11067  
  21         96  
9 21     21   10007 use Data::MuForm::Field::PrimaryKey;
  21         41  
  21         604  
10 21     21   102 use Data::MuForm::Merge ('merge');
  21         29  
  21         1250  
11 21     21   136 use Data::Clone ('data_clone');
  21         25  
  21         821  
12 21     21   75 use Types::Standard -types;
  21         21  
  21         98  
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 95 sub has_init_instance { exists $_[0]->{init_instance} && scalar keys %{$_[0]->{init_instance}} }
  24         107  
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 850 sub is_repeatable {1}
29             #has '+widget' => ( default => 'Repeatable' );
30              
31             sub fields_validate {
32 26     26 0 32 my $self = shift;
33             # loop through array of fields and validate
34 26         30 my @value_array;
35 26         72 foreach my $field ( $self->all_fields ) {
36 53 50       158 next if ( ! $field->active );
37             # Validate each field and "inflate" input -> value.
38 53         176 $field->field_validate; # this calls the field's 'validate' routine
39 53 100       403 push @value_array, $field->value if $field->has_value;
40             }
41 26         149 $self->value( \@value_array );
42             }
43              
44             sub init_state {
45 85     85 0 104 my $self = shift;
46              
47             # must clear out instances built last time
48 85 100       250 unless ( $self->has_contains ) {
49 36 100 66     153 if ( $self->num_fields == 1 && $self->field('contains') ) {
50 12         30 $self->field('contains')->is_contains(1);
51 12         33 $self->contains( $self->field('contains') );
52             }
53             else {
54 24         80 $self->contains( $self->create_element );
55             }
56             }
57 85         319 $self->clear_fields;
58             }
59              
60             sub create_element {
61 24     24 0 40 my ($self) = @_;
62              
63 24         23 my $instance;
64 24         480 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       791 $instance_attr->{primary_key} = $self->primary_key
74             if $self->has_primary_key;
75 24 100       110 if( $self->has_init_instance ) {
76 1         16 $instance_attr = merge( $self->init_instance, $instance_attr );
77             }
78 24 50       382 if( $self->form ) {
79 24         407 $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         96 $instance->push_field( $self->all_fields );
89 24         81 foreach my $fld ( $instance->all_fields ) {
90 72         1263 $fld->parent($instance);
91             }
92              
93             # set required flag
94 24         280 $instance->required( $self->required );
95              
96 24         57 $_->parent($instance) for $instance->all_fields;
97 24         950 return $instance;
98             }
99              
100             sub clone_element {
101 130     130 0 145 my ( $self, $index ) = @_;
102              
103 130         644 my $field = data_clone($self->contains);
104 130         543 $field->clear_errors;
105 130         397 $field->clear_error_fields;
106 130         301 $field->name($index);
107 130         2467 $field->parent($self);
108 130 100       1115 if ( $field->has_fields ) {
109 98         257 $self->clone_fields( $field, [ $field->all_fields ] );
110             }
111 130         1633 return $field;
112             }
113              
114             sub clone_fields {
115 104     104 0 118 my ( $self, $parent, $fields ) = @_;
116              
117 104         97 my @field_array;
118 104         310 $parent->fields( [] );
119 104         3009 foreach my $field ( @{$fields} ) {
  104         179  
120 317         894 my $new_field = data_clone($field);
121 317         1500 $new_field->clear_errors;
122 317         757 $new_field->clear_error_fields;
123 317 100       656 if ( $new_field->has_fields ) {
124 6         18 $self->clone_fields( $new_field, [ $new_field->all_fields ] );
125             }
126 317         6188 $new_field->parent($parent);
127 317         2697 $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 42 my ( $self, $input ) = @_;
134              
135 29         66 $self->init_state;
136 29         1029 $self->input($input);
137             # if Repeatable has array input, need to build instances
138 29         107 $self->fields( [] );
139 29         1037 my $index = 0;
140 29 100       80 if ( ref $input eq 'ARRAY' ) {
141             # build appropriate instance array
142 27         31 foreach my $element ( @{$input} ) {
  27         51  
143 56 100       109 next if not defined $element; # skip empty slots
144 55         102 my $field = $self->clone_element($index);
145 55         784 $field->fill_from_params( $element, 1 );
146 55         193 $self->push_field($field);
147 55         81 $index++;
148             }
149             }
150 29         70 $self->index($index);
151 29 50       77 $self->_setup_for_js if $self->setup_for_js;
152 29         48 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 19 my ( $self, $values ) = @_;
176              
177 15 50 33     103 return $self->fill_from_fields()
178             if ( $self->num_when_empty > 0 && !$values );
179 15         74 $self->obj($values);
180 15         31 $self->init_state;
181             # Create field instances and fill with values
182 15         403 my $index = 0;
183 15         133 my @new_values;
184 15         57 $self->fields( [] );
185 15 50 33     960 $values = [$values] if ( $values && ref $values ne 'ARRAY' );
186 15         18 foreach my $element ( @{$values} ) {
  15         31  
187 32 50       60 next unless $element;
188 32         57 my $field = $self->clone_element($index);
189 32 100       118 if( $field->has_transform_default_to_value ) {
190 2         7 $element = $field->transform_default_to_value->($field, $element);
191             }
192 32         509 $field->fill_from_object( $element );
193 32         54 push @new_values, $field->value;
194 32         119 $self->push_field($field);
195 32         45 $index++;
196             }
197 15 100       72 if( my $num_extra = $self->num_extra ) {
198 1         3 while ($num_extra ) {
199 1         2 $self->_add_extra($index);
200 1         1 $num_extra--;
201 1         3 $index++;
202             }
203             }
204 15         36 $self->index($index);
205 15 50       36 $self->_setup_for_js if $self->setup_for_js;
206 15 100       41 $values = \@new_values if scalar @new_values;
207 15         40 $self->value($values);
208 15         33 return;
209             }
210              
211             sub _add_extra {
212 2     2   2 my ($self, $index) = @_;
213              
214 2         5 my $field = $self->clone_element($index);
215 2         10 $field->fill_from_fields();
216 2         7 $self->push_field($field);
217 2         2 return $field;
218             }
219              
220             sub add_extra {
221 1     1 1 7 my ( $self, $count ) = @_;
222 1 50       3 $count = 1 if not defined $count;
223 1         7 my $index = $self->index;
224 1         3 while ( $count ) {
225 1         4 $self->_add_extra($index);
226 1         1 $count--;
227 1         3 $index++;
228             }
229 1         5 $self->index($index);
230             }
231              
232             # create an empty field
233             sub fill_from_fields {
234 42     42 0 64 my ( $self, ) = @_;
235              
236             # check for defaults
237 42 100       155 if ( my @values = $self->get_default_value ) {
238 2         30 return $self->fill_from_object( \@values );
239             }
240 40         131 $self->init_state;
241 40         335 my $count = $self->num_when_empty;
242 40         50 my $index = 0;
243             # build empty instance
244 40         188 $self->fields( [] );
245 40         8595 while ( $count > 0 ) {
246 41         124 my $field = $self->clone_element($index);
247 41         195 $field->fill_from_fields();
248 41         136 $self->push_field($field);
249 41         42 $index++;
250 41         106 $count--;
251             }
252 40         100 $self->index($index);
253 40 50       116 $self->_setup_for_js if $self->setup_for_js;
254 40         63 return;
255             }
256              
257             sub render {
258 4     4 0 22 my ( $self, $rargs ) = @_;
259 4         35 my $render_args = $self->get_render_args(%$rargs);
260 4         91 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.03
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