File Coverage

blib/lib/HTML/FormHandler/Field/Repeatable.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package HTML::FormHandler::Field::Repeatable;
2             # ABSTRACT: repeatable (array) field
3              
4 1     1   2881 use Moose;
  0            
  0            
5             extends 'HTML::FormHandler::Field::Compound';
6              
7             use aliased 'HTML::FormHandler::Field::Repeatable::Instance';
8             use HTML::FormHandler::Field::PrimaryKey;
9             use HTML::FormHandler::Merge ('merge');
10             use Data::Clone ('data_clone');
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             my $self = shift;
34             # loop through array of fields and validate
35             my @value_array;
36             foreach my $field ( $self->all_fields ) {
37             next if ( $field->is_inactive );
38             # Validate each field and "inflate" input -> value.
39             $field->validate_field; # this calls the field's 'validate' routine
40             push @value_array, $field->value if $field->has_value;
41             }
42             $self->_set_value( \@value_array );
43             }
44              
45             sub init_state {
46             my $self = shift;
47              
48             # must clear out instances built last time
49             unless ( $self->has_contains ) {
50             if ( $self->num_fields == 1 && $self->field('contains') ) {
51             $self->field('contains')->is_contains(1);
52             $self->contains( $self->field('contains') );
53             }
54             else {
55             $self->contains( $self->create_element );
56             }
57             }
58             $self->clear_fields;
59             }
60              
61             sub create_element {
62             my ($self) = @_;
63              
64             my $instance;
65             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             $instance_attr->{primary_key} = $self->primary_key
73             if $self->has_primary_key;
74             if( $self->has_init_contains ) {
75             $instance_attr = merge( $self->init_contains, $instance_attr );
76             }
77             if( $self->form ) {
78             $instance_attr->{form} = $self->form;
79             $instance = $self->form->_make_adhoc_field(
80             'HTML::FormHandler::Field::Repeatable::Instance',
81             $instance_attr );
82             }
83             else {
84             $instance = Instance->new( %$instance_attr );
85             }
86             # copy the fields from this field into the instance
87             $instance->add_field( $self->all_fields );
88             foreach my $fld ( $instance->all_fields ) {
89             $fld->parent($instance);
90             }
91              
92             # set required flag
93             $instance->required( $self->required );
94              
95             # auto_id has no way to change widgets...deprecate this?
96             if ( $self->auto_id ) {
97             unless ( grep $_->can('is_primary_key') && $_->is_primary_key, $instance->all_fields ) {
98             my $field;
99             my $field_attr = { name => 'id', parent => $instance };
100             if ( $self->form ) { # this will pull in the widget role
101             $field_attr->{form} = $self->form;
102             $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             $field = HTML::FormHandler::Field::PrimaryKey->new( %$field_attr );
107             }
108             $instance->add_field($field);
109             }
110             }
111             $_->parent($instance) for $instance->all_fields;
112             return $instance;
113             }
114              
115             sub clone_element {
116             my ( $self, $index ) = @_;
117              
118             my $field = $self->contains->clone( errors => [], error_fields => [] );
119             $field->name($index);
120             $field->parent($self);
121             if ( $field->has_fields ) {
122             $self->clone_fields( $field, [ $field->all_fields ] );
123             }
124             return $field;
125             }
126              
127             sub clone_fields {
128             my ( $self, $parent, $fields ) = @_;
129             my @field_array;
130             $parent->fields( [] );
131             foreach my $field ( @{$fields} ) {
132             my $new_field = $field->clone( errors => [], error_fields => [] );
133             if ( $new_field->has_fields ) {
134             $self->clone_fields( $new_field, [ $new_field->all_fields ] );
135             }
136             $new_field->parent($parent);
137             $parent->add_field($new_field);
138             }
139             }
140              
141             # params exist and validation will be performed (later)
142             sub _result_from_input {
143             my ( $self, $result, $input ) = @_;
144              
145             $self->init_state;
146             $result->_set_input($input);
147             $self->_set_result($result);
148             # if Repeatable has array input, need to build instances
149             $self->fields( [] );
150             my $index = 0;
151             if ( ref $input eq 'ARRAY' ) {
152             # build appropriate instance array
153             foreach my $element ( @{$input} ) {
154             next if not defined $element; # skip empty slots
155             my $field = $self->clone_element($index);
156             my $result = HTML::FormHandler::Field::Result->new(
157             name => $index,
158             parent => $self->result
159             );
160             $result = $field->_result_from_input( $result, $element, 1 );
161             $self->result->add_result($result);
162             $self->add_field($field);
163             $index++;
164             }
165             }
166             $self->index($index);
167             $self->_setup_for_js if $self->setup_for_js;
168             $self->result->_set_field_def($self);
169             return $self->result;
170             }
171              
172             sub _setup_for_js {
173             my $self = shift;
174             return unless $self->form;
175             my $full_name = $self->full_name;
176             my $index_level =()= $full_name =~ /{index\d+}/g;
177             $index_level++;
178             my $field_name = "{index-$index_level}";
179             my $field = $self->_add_extra($field_name);
180             my $rendered = $field->render;
181             # remove extra result & field, now that it's rendered
182             $self->result->_pop_result;
183             $self->_pop_field;
184             # set the information in the form
185             # $self->index is the index of the next instance
186             $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             my ( $self, $result, $values ) = @_;
193              
194             return $self->_result_from_fields($result)
195             if ( $self->num_when_empty > 0 && !$values );
196             $self->item($values);
197             $self->init_state;
198             $self->_set_result($result);
199             # Create field instances and fill with values
200             my $index = 0;
201             my @new_values;
202             $self->fields( [] );
203             $values = [$values] if ( $values && ref $values ne 'ARRAY' );
204             foreach my $element ( @{$values} ) {
205             next unless $element;
206             my $field = $self->clone_element($index);
207             my $result =
208             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
209             if( $field->has_inflate_default_method ) {
210             $element = $field->inflate_default($element);
211             }
212             $result = $field->_result_from_object( $result, $element );
213             push @new_values, $result->value;
214             $self->add_field($field);
215             $self->result->add_result( $field->result );
216             $index++;
217             }
218             if( my $num_extra = $self->num_extra ) {
219             while ($num_extra ) {
220             $self->_add_extra($index);
221             $num_extra--;
222             $index++;
223             }
224             }
225             $self->index($index);
226             $self->_setup_for_js if $self->setup_for_js;
227             $values = \@new_values if scalar @new_values;
228             $self->_set_value($values);
229             $self->result->_set_field_def($self);
230             return $self->result;
231             }
232              
233             sub _add_extra {
234             my ($self, $index) = @_;
235              
236             my $field = $self->clone_element($index);
237             my $result =
238             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
239             $result = $field->_result_from_fields($result);
240             $self->result->add_result($result) if $result;
241             $self->add_field($field);
242             return $field;
243             }
244              
245             sub add_extra {
246             my ( $self, $count ) = @_;
247             $count = 1 if not defined $count;
248             my $index = $self->index;
249             while ( $count ) {
250             $self->_add_extra($index);
251             $count--;
252             $index++;
253             }
254             $self->index($index);
255             }
256              
257             # create an empty field
258             sub _result_from_fields {
259             my ( $self, $result ) = @_;
260              
261             # check for defaults
262             if ( my @values = $self->get_default_value ) {
263             return $self->_result_from_object( $result, \@values );
264             }
265             $self->init_state;
266             $self->_set_result($result);
267             my $count = $self->num_when_empty;
268             my $index = 0;
269             # build empty instance
270             $self->fields( [] );
271             while ( $count > 0 ) {
272             my $field = $self->clone_element($index);
273             my $result =
274             HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
275             $result = $field->_result_from_fields($result);
276             $self->result->add_result($result) if $result;
277             $self->add_field($field);
278             $index++;
279             $count--;
280             }
281             $self->index($index);
282             $self->_setup_for_js if $self->setup_for_js;
283             $self->result->_set_field_def($self);
284             return $result;
285             }
286              
287             __PACKAGE__->meta->make_immutable;
288             use namespace::autoclean;
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.40057
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 num_when_empty
397              
398             This attribute (default 1) indicates how many empty fields to present
399             in an empty form which hasn't been filled from parameters or database
400             rows.
401              
402             =item num_extra
403              
404             When the field results are built from an existing object (item or init_object)
405             an additional number of repeatable elements will be created equal to this
406             number. Default is 0.
407              
408             =item add_extra
409              
410             When a form is submitted and the field results are built from the input
411             parameters, it's not clear when or if an additional repeatable element might
412             be wanted. The method 'add_extra' will add an empty repeatable element.
413              
414             $form->process( params => {....} );
415             $form->field('my_repeatable')->add_extra(1);
416              
417             This might be useful if the form is being re-presented to the user.
418              
419             =item setup_for_js
420              
421             setup_for_js => 1
422              
423             Saves information in the form for javascript to use when adding repeatable elements.
424             If using the example javascript, you also must set 'do_wrapper' in the
425             Repeatable field and use the Bootstrap widget wrapper (or wrap the repeatable
426             elements in a 'controls' div by setting tags => { controls_div => 1 }.
427             See t/repeatable/js.t for an example. See also
428             L<HTML::FormHandler::Render::RepeatableJs> and L<HTML::FormHandler::Field::AddElement>.
429              
430             =back
431              
432             =head1 AUTHOR
433              
434             FormHandler Contributors - see HTML::FormHandler
435              
436             =head1 COPYRIGHT AND LICENSE
437              
438             This software is copyright (c) 2014 by Gerda Shank.
439              
440             This is free software; you can redistribute it and/or modify it under
441             the same terms as the Perl 5 programming language system itself.
442              
443             =cut