File Coverage

blib/lib/HTML/FormFu/Element/Repeatable.pm
Criterion Covered Total %
statement 154 154 100.0
branch 35 44 79.5
condition 9 14 64.2
subroutine 16 16 100.0
pod 2 5 40.0
total 216 233 92.7


line stmt bran cond sub pod time code
1 35     35   1156 use strict;
  35         84  
  35         2426  
2              
3             package HTML::FormFu::Element::Repeatable;
4             $HTML::FormFu::Element::Repeatable::VERSION = '2.07';
5             # ABSTRACT: repeatable block element
6              
7 35     35   219 use Moose;
  35         75  
  35         297  
8 35     35   248853 use MooseX::Attribute::Chained;
  35         134  
  35         1528  
9             extends 'HTML::FormFu::Element::Block';
10              
11 35     35   225 use HTML::FormFu::Util qw( DEBUG_PROCESS debug );
  35         80  
  35         2661  
12 35     35   249 use List::Util qw( first );
  35         87  
  35         2362  
13 35     35   243 use Carp qw( croak );
  35         113  
  35         59820  
14              
15             has counter_name => ( is => 'rw', traits => ['Chained'] );
16              
17             has _original_elements => ( is => 'rw' );
18              
19             has increment_field_names => (
20             is => 'rw',
21             default => 1,
22             lazy => 1,
23             traits => ['Chained'],
24             );
25              
26             # This attribute is currently not documented as FF::Model::HashRef
27             # only supports '_'
28              
29             has repeatable_delimiter => (
30             is => 'rw',
31             default => '_',
32             lazy => 1,
33             traits => ['Chained'],
34             );
35              
36             after BUILD => sub {
37             my $self = shift;
38              
39             $self->filename('repeatable');
40             $self->is_repeatable(1);
41              
42             return;
43             };
44              
45             sub repeat {
46 47     47 1 839 my ( $self, $count ) = @_;
47              
48 47 50       553 croak "invalid number to repeat"
49             if $count !~ /^[0-9]+\z/;
50              
51 47         767 my $children;
52              
53 47 100       1964 if ( $self->_original_elements ) {
54              
55             # repeat() has already been called
56 6         190 $children = $self->_original_elements;
57             }
58             else {
59 41         1341 $children = $self->_elements;
60              
61 41         1358 $self->_original_elements($children);
62             }
63              
64 47 50       219 croak "no child elements to repeat"
65             if !@$children;
66              
67 47         1410 $self->_elements( [] );
68              
69 47 100       184 return [] if !$count;
70              
71             # switch behaviour
72             # If nested_name is set, we add the repeatable counter to the name
73             # of the containing block (this repeatable block).
74             # This behaviour eases the creation of client side javascript code
75             # to add and remove repeatable elements client side.
76             # If nested_name is *not* set, we add the repeatable counter to the names
77             # of the child elements (leaves of the element tree).
78 46         1525 my $nested_name = $self->nested_name;
79 46 100 66     337 if ( defined $nested_name && length $nested_name ) {
80 19         94 return $self->_repeat_containing_block($count);
81             }
82             else {
83 27         132 return $self->_repeat_child_elements($count);
84             }
85             }
86              
87             sub _repeat_containing_block {
88 19     19   62 my ( $self, $count ) = @_;
89              
90 19         647 my $children = $self->_original_elements;
91              
92             # We must not get 'nested.nested_1' instead of 'nested_1' through the
93             # nested_name attribute of the Repeatable element, thus we extended
94             # FF::Elements::_Field nested_names method to ignore Repeatable elements.
95 19         556 my $nested_name = $self->nested_name;
96 19         738 $self->original_nested_name($nested_name);
97              
98             # delimiter between nested_name and the incremented counter
99 19         667 my $delimiter = $self->repeatable_delimiter;
100              
101 19         47 my @return;
102              
103 19         74 for my $rep ( 1 .. $count ) {
104              
105             # create clones of elements and put them in a new block
106 36         101 my @clones = map { $_->clone } @$children;
  68         296  
107 36         228 my $block = $self->element('Block');
108              
109             # initiate new block with properties of this repeatable
110 36         1077 $block->_elements( \@clones );
111 36         144 $block->attributes( $self->attributes );
112 36         1096 $block->tag( $self->tag );
113              
114 36         180 $block->repeatable_count($rep);
115              
116 36 50       1197 if ( $self->increment_field_names ) {
117              
118             # store the original nested_name attribute for later usage when
119             # building the original nested name
120 36 50       1214 $block->original_nested_name( $block->nested_name )
121             if !defined $block->original_nested_name;
122              
123             # create new nested name with repeat counter
124 36         1135 $block->nested_name( $nested_name . $delimiter . $rep );
125              
126 36         70 for my $field ( @{ $block->get_fields } ) {
  36         157  
127              
128 78 50       274 if ( defined( my $name = $field->name ) ) {
129              
130             # store original name for later usage when
131             # replacing the field names in constraints
132 78 100       2506 $field->original_name($name)
133             if !defined $field->original_name;
134              
135             # store original nested name for later usage when
136             # replacing the field names in constraints
137 78 100       2446 $field->original_nested_name(
138             $field->build_original_nested_name )
139             if !defined $field->original_nested_name;
140             }
141             }
142             }
143              
144 36         171 _reparent_children($block);
145              
146 36         104 my @fields = @{ $block->get_fields };
  36         120  
147              
148 36         123 for my $field (@fields) {
149 19         76 map { $_->parent($field) }
150 78         2511 @{ $field->_deflators },
151 78         2189 @{ $field->_filters },
152 78         2365 @{ $field->_constraints },
153 78         2190 @{ $field->_inflators },
154 78         2315 @{ $field->_validators },
155 78         2241 @{ $field->_transformers },
156 78         129 @{ $field->_plugins },
  78         2170  
157             ;
158             }
159              
160 36         92 for my $field (@fields) {
161 13         64 map { $_->repeatable_repeat( $self, $block ) }
162 78         150 @{ $field->_constraints };
  78         2169  
163             }
164              
165 36         130 push @return, $block;
166             }
167              
168 19         95 return \@return;
169             }
170              
171             sub get_field_with_original_name {
172 11     11 0 32 my ( $self, $name, $fields ) = @_;
173              
174 23     23   682 my $field = first { $_->original_nested_name eq $name }
175 11         53 grep { defined $_->original_nested_name } @$fields;
  26         824  
176              
177 4     4   112 $field ||= first { $_->original_name eq $name }
178 11   66     63 grep { defined $_->original_name } @$fields;
  6         173  
179              
180 11         30 return $field;
181             }
182              
183             sub _repeat_child_elements {
184 27     27   92 my ( $self, $count ) = @_;
185              
186 27         857 my $children = $self->_original_elements;
187              
188             # delimiter between nested_name and the incremented counter
189 27         917 my $delimiter = $self->repeatable_delimiter;
190              
191 27         67 my @return;
192              
193 27         119 for my $rep ( 1 .. $count ) {
194 51         126 my @clones = map { $_->clone } @$children;
  92         434  
195 51         366 my $block = $self->element('Block');
196              
197 51         1515 $block->_elements( \@clones );
198 51         221 $block->attributes( $self->attributes );
199 51         1533 $block->tag( $self->tag );
200              
201 51         248 $block->repeatable_count($rep);
202              
203 51 100       1822 if ( $self->increment_field_names ) {
204 42         87 for my $field ( @{ $block->get_fields } ) {
  42         205  
205              
206 83 50       253 if ( defined( my $name = $field->name ) ) {
207 83 100       2687 $field->original_name($name)
208             if !defined $field->original_name;
209              
210 83 100       2641 $field->original_nested_name( $field->nested_name )
211             if !defined $field->original_nested_name;
212              
213 83         364 $field->name( ${name} . $delimiter . $rep );
214             }
215             }
216             }
217              
218 51         235 _reparent_children($block);
219              
220 51         118 my @fields = @{ $block->get_fields };
  51         167  
221              
222 51         180 for my $field (@fields) {
223 39         122 map { $_->parent($field) }
224 96         2904 @{ $field->_deflators },
225 96         2740 @{ $field->_filters },
226 96         2823 @{ $field->_constraints },
227 96         2802 @{ $field->_inflators },
228 96         2731 @{ $field->_validators },
229 96         2782 @{ $field->_transformers },
230 96         162 @{ $field->_plugins },
  96         2725  
231             ;
232             }
233              
234 51         137 for my $field (@fields) {
235 36         161 map { $_->repeatable_repeat( $self, $block ) }
236 96         152 @{ $field->_constraints };
  96         2797  
237             }
238              
239 51         183 push @return, $block;
240             }
241              
242 27         143 return \@return;
243             }
244              
245             sub _reparent_children {
246 277     277   489 my $self = shift;
247              
248 277 100       7438 return if !$self->is_block;
249              
250 112         222 for my $child ( @{ $self->get_elements } ) {
  112         465  
251 190         717 $child->parent($self);
252              
253 190         548 _reparent_children($child);
254             }
255             }
256              
257             sub process {
258 54     54 0 147 my $self = shift;
259              
260 54         1821 my $counter_name = $self->counter_name;
261 54         392 my $form = $self->form;
262 54         131 my $count = 1;
263              
264 54 100 66     3295 if ( defined $counter_name && defined $form->query ) {
265              
266             # are we in a nested-repeatable?
267 44         111 my $parent = $self;
268              
269 44         182 while ( defined( $parent = $parent->parent ) ) {
270 49         370 my $field
271             = $parent->get_field( { original_name => $counter_name } );
272              
273 49 100       275 if ( defined $field ) {
274 10         48 $counter_name = $field->nested_name;
275 10         30 last;
276             }
277             }
278              
279 44         1171 my $input = $form->query->param($counter_name);
280              
281 44 100 66     2707 if ( defined $input && $input =~ /^[1-9][0-9]*\z/ ) {
282 43         136 $count = $input;
283             }
284             }
285              
286 54 100       1972 if ( !$self->_original_elements ) {
287 20 50       100 DEBUG_PROCESS && debug("calling \$repeatable->repeat($count)");
288              
289 20         104 $self->repeat($count);
290             }
291              
292 54         449 return $self->SUPER::process(@_);
293             }
294              
295             sub content {
296 29     29 1 85 my $self = shift;
297              
298 29 50       110 croak "Repeatable elements do not support the content() method"
299             if @_;
300              
301 29         114 return;
302             }
303              
304             sub string {
305 22     22 0 75 my ( $self, $args ) = @_;
306              
307 22   50     412 $args ||= {};
308              
309             my $render
310             = exists $args->{render_data}
311             ? $args->{render_data}
312 22 50       205 : $self->render_data_non_recursive;
313              
314             # block template
315              
316 22         58 my @divs = map { $_->render } @{ $self->get_elements };
  40         222  
  22         102  
317              
318 22         110 my $html = join "\n", @divs;
319              
320 22         153 return $html;
321             }
322              
323             __PACKAGE__->meta->make_immutable;
324              
325             1;
326              
327             __END__
328              
329             =pod
330              
331             =encoding UTF-8
332              
333             =head1 NAME
334              
335             HTML::FormFu::Element::Repeatable - repeatable block element
336              
337             =head1 VERSION
338              
339             version 2.07
340              
341             =head1 SYNOPSIS
342              
343             ---
344             elements:
345             - type: Repeatable
346             name: my_rep
347             elements:
348             - name: foo
349             - name: bar
350              
351             Calling C<< $element->repeat(2) >> would result in the following markup:
352              
353             <div>
354             <input name="my_rep.foo_1" type="text" />
355             <input name="my_rep.bar_1" type="text" />
356             </div>
357             <div>
358             <input name="my_rep.foo_2" type="text" />
359             <input name="my_rep.bar_2" type="text" />
360             </div>
361              
362             Example of constraints:
363              
364             ----
365             elements:
366             - type: Repeatable
367             name: my_rep
368             elements:
369             - name: id
370              
371             - name: foo
372             constraints:
373             - type: Required
374             when:
375             field: 'my_rep.id' # use full nested-name
376              
377             - name: bar
378             constraints:
379             - type: Equal
380             others: 'my_rep.foo' # use full nested-name
381              
382             =head1 DESCRIPTION
383              
384             Provides a way to extend a form at run-time, by copying and repeating its
385             child elements.
386              
387             The elements intended for copying must be added before L</repeat> is called.
388              
389             Although the Repeatable element inherits from
390             L<Block|HTML::FormFu::Element::Block>, it doesn't generate a block tag
391             around all the repeated elements - instead it places each repeat of the
392             elements in a new L<Block|HTML::FormFu::Element::Block> element, which
393             inherits the Repeatable's display settings, such as L</attributes> and
394             L</tag>.
395              
396             For all constraints attached to fields within a Repeatable block which use
397             either L<others|HTML::FormFu::Role::Constraint::Others/others> or
398             L<when|HTML::FormFu::Constraint/when> containing names of fields within
399             the same Repeatable block, when L<repeat> is called, those names will
400             automatically be updated to the new nested-name for each field (taking
401             into account L<increment_field_names>).
402              
403             =head1 METHODS
404              
405             =head2 repeat
406              
407             Arguments: [$count]
408              
409             Return Value: $arrayref_of_new_child_blocks
410              
411             This method creates C<$count> number of copies of the child elements.
412             If no argument C<$count> is provided, it defaults to C<1>.
413              
414             Note that C<< $form->process >> will call L</repeat> automatically to ensure the
415             initial child elements are correctly set up - unless you call L</repeat>
416             manually first, in which case the child elements you created will be left
417             untouched (otherwise L<process|HTML::FormFu/process> would overwrite your
418             changes).
419              
420             Any subsequent call to L</repeat> will delete the previously copied elements
421             before creating new copies - this means you cannot make repeated calls to
422             L</repeat> within a loop to create more copies.
423              
424             Each copy of the elements returned are contained in a new
425             L<Block|HTML::FormFu::Element::Block> element. For example, calling
426             C<< $element->repeat(2) >> on a Repeatable element containing 2 Text fields
427             would return 2 L<Block|HTML::FormFu::Element::Block> elements, each
428             containing a copy of the 2 Text fields.
429              
430             =head2 counter_name
431              
432             Arguments: $name
433              
434             If true, the L<HTML::FormFu/query> will be searched during
435             L<HTML::FormFu/process> for a parameter with the given name. The value for
436             that parameter will be passed to L</repeat>, to automatically create the
437             new copies.
438              
439             If L</increment_field_names> is true (the default), this is essential: if the
440             elements corresponding to the new fieldnames (foo_1, bar_2, etc.) are not
441             present on the form during L<HTML::FormFu/process>, no Processors
442             (Constraints, etc.) will be run on the fields, and their values will not
443             be returned by L<HTML::FormFu/params> or L<HTML::FormFu/param>.
444              
445             =head2 increment_field_names
446              
447             Arguments: $bool
448              
449             Default Value: 1
450              
451             If true, then all fields will have C<< _n >> appended to their name, where
452             C<n> is the L</repeatable_count> value.
453              
454             =head2 repeatable_count
455              
456             This is set on each new L<Block|HTML::FormFu::Element::Block> element
457             returned by L</repeat>, starting at number C<1>.
458              
459             Because this is an 'inherited accessor' available on all elements, it can be
460             used to determine whether any element is a child of a Repeatable element.
461              
462             Only available after L<repeat> has been called.
463              
464             =head2 repeatable_count_no_inherit
465              
466             A non-inheriting variant of L</repeatable_count>.
467              
468             =head2 nested_name
469              
470             If the L</nested_name> attribute is set, the naming scheme of the Repeatable
471             element's children is switched to add the counter to the repeatable blocks
472             themselves.
473              
474             ---
475             elements:
476             - type: Repeatable
477             nested_name: my_rep
478             elements:
479             - name: foo
480             - name: bar
481              
482             Calling C<< $element->repeat(2) >> would result in the following markup:
483              
484             <div>
485             <input name="my_rep_1.foo" type="text" />
486             <input name="my_rep_1.bar" type="text" />
487             </div>
488             <div>
489             <input name="my_rep_2.foo" type="text" />
490             <input name="my_rep_2.bar" type="text" />
491             </div>
492              
493             Because this is an 'inherited accessor' available on all elements, it can be
494             used to determine whether any element is a child of a Repeatable element.
495              
496             =head2 attributes
497              
498             =head2 attrs
499              
500             Any attributes set will be passed to every repeated Block of elements.
501              
502             ---
503             elements:
504             - type: Repeatable
505             name: my_rep
506             attributes:
507             class: rep
508             elements:
509             - name: foo
510              
511             Calling C<< $element->repeat(2) >> would result in the following markup:
512              
513             <div class="rep">
514             <input name="my_rep.foo_1" type="text" />
515             </div>
516             <div class="rep">
517             <input name="my_rep.foo_2" type="text" />
518             </div>
519              
520             See L<HTML::FormFu/attributes> for details.
521              
522             =head2 tag
523              
524             The L</tag> value will be passed to every repeated Block of elements.
525              
526             ---
527             elements:
528             - type: Repeatable
529             name: my_rep
530             tag: span
531             elements:
532             - name: foo
533              
534             Calling C<< $element->repeat(2) >> would result in the following markup:
535              
536             <span>
537             <input name="my_rep.foo_1" type="text" />
538             </span>
539             <span>
540             <input name="my_rep.foo_2" type="text" />
541             </span>
542              
543             See L<HTML::FormFu::Element::Block/tag> for details.
544              
545             =head2 auto_id
546              
547             As well as the usual substitutions, any instances of C<%r> will be
548             replaced with the value of L</repeatable_count>.
549              
550             See L<HTML::FormFu::Element::Block/auto_id> for further details.
551              
552             ---
553             elements:
554             - type: Repeatable
555             name: my_rep
556             auto_id: "%n_%r"
557             elements:
558             - name: foo
559              
560             Calling C<< $element->repeat(2) >> would result in the following markup:
561              
562             <div>
563             <input name="my_rep.foo_1" id="foo_1" type="text" />
564             </div>
565             <div>
566             <input name="my_rep.foo_2" id="foo_2" type="text" />
567             </div>
568              
569             =head2 content
570              
571             Not supported for Repeatable elements - will throw a fatal error if called as
572             a setter.
573              
574             =head1 CAVEATS
575              
576             =head2 Unsupported Constraints
577              
578             Note that constraints with an L<others|HTML::FormFu::Role::Constraint::Others>
579             method do not work correctly within a Repeatable block. Currently, these are:
580             L<AllOrNone|HTML::FormFu::Constraint::AllOrNone>,
581             L<DependOn|HTML::FormFu::Constraint::DependOn>,
582             L<Equal|HTML::FormFu::Constraint::Equal>,
583             L<MinMaxFields|HTML::FormFu::Constraint::MinMaxFields>,
584             L<reCAPTCHA|HTML::FormFu::Constraint::reCAPTCHA>.
585             Also, the L<CallbackOnce|HTML::FormFu::Constraint::CallbackOnce> constraint
586             won't work within a Repeatable block, as it wouldn't make much sense.
587              
588             =head2 Work-arounds
589              
590             See L<HTML::FormFu::Filter::ForceListValue> to address a problem with
591             L<increment_field_names> disabled, and increading the L<repeat> on the
592             server-side.
593              
594             =head1 SEE ALSO
595              
596             Is a sub-class of, and inherits methods from
597             L<HTML::FormFu::Element::Block>,
598             L<HTML::FormFu::Element>
599              
600             L<HTML::FormFu>
601              
602             =head1 AUTHOR
603              
604             Carl Franks, C<cfranks@cpan.org>
605              
606             =head1 LICENSE
607              
608             This library is free software, you can redistribute it and/or modify it under
609             the same terms as Perl itself.
610              
611             =head1 AUTHOR
612              
613             Carl Franks <cpan@fireartist.com>
614              
615             =head1 COPYRIGHT AND LICENSE
616              
617             This software is copyright (c) 2018 by Carl Franks.
618              
619             This is free software; you can redistribute it and/or modify it under
620             the same terms as the Perl 5 programming language system itself.
621              
622             =cut