1use strict;
2
3package HTML::FormFu::Element::Repeatable;
4$HTML::FormFu::Element::Repeatable::VERSION = '2.07';
5# ABSTRACT: repeatable block element
6
7use Moose;
8use MooseX::Attribute::Chained;
9extends 'HTML::FormFu::Element::Block';
10
11use HTML::FormFu::Util qw( DEBUG_PROCESS debug );
12use List::Util qw( first );
13use Carp qw( croak );
14
15has counter_name => ( is => 'rw', traits => ['Chained'] );
16
17has _original_elements => ( is => 'rw' );
18
19has 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
29has repeatable_delimiter => (
30    is      => 'rw',
31    default => '_',
32    lazy    => 1,
33    traits  => ['Chained'],
34);
35
36after BUILD => sub {
37    my $self = shift;
38
39    $self->filename('repeatable');
40    $self->is_repeatable(1);
41
42    return;
43};
44
45sub repeat {
46    my ( $self, $count ) = @_;
47
48    croak "invalid number to repeat"
49        if $count !~ /^[0-9]+\z/;
50
51    my $children;
52
53    if ( $self->_original_elements ) {
54
55        # repeat() has already been called
56        $children = $self->_original_elements;
57    }
58    else {
59        $children = $self->_elements;
60
61        $self->_original_elements($children);
62    }
63
64    croak "no child elements to repeat"
65        if !@$children;
66
67    $self->_elements( [] );
68
69    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    my $nested_name = $self->nested_name;
79    if ( defined $nested_name && length $nested_name ) {
80        return $self->_repeat_containing_block($count);
81    }
82    else {
83        return $self->_repeat_child_elements($count);
84    }
85}
86
87sub _repeat_containing_block {
88    my ( $self, $count ) = @_;
89
90    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    my $nested_name = $self->nested_name;
96    $self->original_nested_name($nested_name);
97
98    # delimiter between nested_name and the incremented counter
99    my $delimiter = $self->repeatable_delimiter;
100
101    my @return;
102
103    for my $rep ( 1 .. $count ) {
104
105        # create clones of elements and put them in a new block
106        my @clones = map { $_->clone } @$children;
107        my $block = $self->element('Block');
108
109        # initiate new block with properties of this repeatable
110        $block->_elements( \@clones );
111        $block->attributes( $self->attributes );
112        $block->tag( $self->tag );
113
114        $block->repeatable_count($rep);
115
116        if ( $self->increment_field_names ) {
117
118            # store the original nested_name attribute for later usage when
119            # building the original nested name
120            $block->original_nested_name( $block->nested_name )
121                if !defined $block->original_nested_name;
122
123            # create new nested name with repeat counter
124            $block->nested_name( $nested_name . $delimiter . $rep );
125
126            for my $field ( @{ $block->get_fields } ) {
127
128                if ( defined( my $name = $field->name ) ) {
129
130                    # store original name for later usage when
131                    # replacing the field names in constraints
132                    $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                    $field->original_nested_name(
138                        $field->build_original_nested_name )
139                        if !defined $field->original_nested_name;
140                }
141            }
142        }
143
144        _reparent_children($block);
145
146        my @fields = @{ $block->get_fields };
147
148        for my $field (@fields) {
149            map { $_->parent($field) }
150                @{ $field->_deflators },
151                @{ $field->_filters },
152                @{ $field->_constraints },
153                @{ $field->_inflators },
154                @{ $field->_validators },
155                @{ $field->_transformers },
156                @{ $field->_plugins },
157                ;
158        }
159
160        for my $field (@fields) {
161            map { $_->repeatable_repeat( $self, $block ) }
162                @{ $field->_constraints };
163        }
164
165        push @return, $block;
166    }
167
168    return \@return;
169}
170
171sub get_field_with_original_name {
172    my ( $self, $name, $fields ) = @_;
173
174    my $field = first { $_->original_nested_name eq $name }
175    grep { defined $_->original_nested_name } @$fields;
176
177    $field ||= first { $_->original_name eq $name }
178    grep { defined $_->original_name } @$fields;
179
180    return $field;
181}
182
183sub _repeat_child_elements {
184    my ( $self, $count ) = @_;
185
186    my $children = $self->_original_elements;
187
188    # delimiter between nested_name and the incremented counter
189    my $delimiter = $self->repeatable_delimiter;
190
191    my @return;
192
193    for my $rep ( 1 .. $count ) {
194        my @clones = map { $_->clone } @$children;
195        my $block = $self->element('Block');
196
197        $block->_elements( \@clones );
198        $block->attributes( $self->attributes );
199        $block->tag( $self->tag );
200
201        $block->repeatable_count($rep);
202
203        if ( $self->increment_field_names ) {
204            for my $field ( @{ $block->get_fields } ) {
205
206                if ( defined( my $name = $field->name ) ) {
207                    $field->original_name($name)
208                        if !defined $field->original_name;
209
210                    $field->original_nested_name( $field->nested_name )
211                        if !defined $field->original_nested_name;
212
213                    $field->name( ${name} . $delimiter . $rep );
214                }
215            }
216        }
217
218        _reparent_children($block);
219
220        my @fields = @{ $block->get_fields };
221
222        for my $field (@fields) {
223            map { $_->parent($field) }
224                @{ $field->_deflators },
225                @{ $field->_filters },
226                @{ $field->_constraints },
227                @{ $field->_inflators },
228                @{ $field->_validators },
229                @{ $field->_transformers },
230                @{ $field->_plugins },
231                ;
232        }
233
234        for my $field (@fields) {
235            map { $_->repeatable_repeat( $self, $block ) }
236                @{ $field->_constraints };
237        }
238
239        push @return, $block;
240    }
241
242    return \@return;
243}
244
245sub _reparent_children {
246    my $self = shift;
247
248    return if !$self->is_block;
249
250    for my $child ( @{ $self->get_elements } ) {
251        $child->parent($self);
252
253        _reparent_children($child);
254    }
255}
256
257sub process {
258    my $self = shift;
259
260    my $counter_name = $self->counter_name;
261    my $form         = $self->form;
262    my $count        = 1;
263
264    if ( defined $counter_name && defined $form->query ) {
265
266        # are we in a nested-repeatable?
267        my $parent = $self;
268
269        while ( defined( $parent = $parent->parent ) ) {
270            my $field
271                = $parent->get_field( { original_name => $counter_name } );
272
273            if ( defined $field ) {
274                $counter_name = $field->nested_name;
275                last;
276            }
277        }
278
279        my $input = $form->query->param($counter_name);
280
281        if ( defined $input && $input =~ /^[1-9][0-9]*\z/ ) {
282            $count = $input;
283        }
284    }
285
286    if ( !$self->_original_elements ) {
287        DEBUG_PROCESS && debug("calling \$repeatable->repeat($count)");
288
289        $self->repeat($count);
290    }
291
292    return $self->SUPER::process(@_);
293}
294
295sub content {
296    my $self = shift;
297
298    croak "Repeatable elements do not support the content() method"
299        if @_;
300
301    return;
302}
303
304sub string {
305    my ( $self, $args ) = @_;
306
307    $args ||= {};
308
309    my $render
310        = exists $args->{render_data}
311        ? $args->{render_data}
312        : $self->render_data_non_recursive;
313
314    # block template
315
316    my @divs = map { $_->render } @{ $self->get_elements };
317
318    my $html = join "\n", @divs;
319
320    return $html;
321}
322
323__PACKAGE__->meta->make_immutable;
324
3251;
326
327__END__
328
329=pod
330
331=encoding UTF-8
332
333=head1 NAME
334
335HTML::FormFu::Element::Repeatable - repeatable block element
336
337=head1 VERSION
338
339version 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
351Calling 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
362Example 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
384Provides a way to extend a form at run-time, by copying and repeating its
385child elements.
386
387The elements intended for copying must be added before L</repeat> is called.
388
389Although the Repeatable element inherits from
390L<Block|HTML::FormFu::Element::Block>, it doesn't generate a block tag
391around all the repeated elements - instead it places each repeat of the
392elements in a new L<Block|HTML::FormFu::Element::Block> element, which
393inherits the Repeatable's display settings, such as L</attributes> and
394L</tag>.
395
396For all constraints attached to fields within a Repeatable block which use
397either L<others|HTML::FormFu::Role::Constraint::Others/others> or
398L<when|HTML::FormFu::Constraint/when> containing names of fields within
399the same Repeatable block, when L<repeat> is called, those names will
400automatically be updated to the new nested-name for each field (taking
401into account L<increment_field_names>).
402
403=head1 METHODS
404
405=head2 repeat
406
407Arguments: [$count]
408
409Return Value: $arrayref_of_new_child_blocks
410
411This method creates C<$count> number of copies of the child elements.
412If no argument C<$count> is provided, it defaults to C<1>.
413
414Note that C<< $form->process >> will call L</repeat> automatically to ensure the
415initial child elements are correctly set up - unless you call L</repeat>
416manually first, in which case the child elements you created will be left
417untouched (otherwise L<process|HTML::FormFu/process> would overwrite your
418changes).
419
420Any subsequent call to L</repeat> will delete the previously copied elements
421before creating new copies - this means you cannot make repeated calls to
422L</repeat> within a loop to create more copies.
423
424Each copy of the elements returned are contained in a new
425L<Block|HTML::FormFu::Element::Block> element. For example, calling
426C<< $element->repeat(2) >> on a Repeatable element containing 2 Text fields
427would return 2 L<Block|HTML::FormFu::Element::Block> elements, each
428containing a copy of the 2 Text fields.
429
430=head2 counter_name
431
432Arguments: $name
433
434If true, the L<HTML::FormFu/query> will be searched during
435L<HTML::FormFu/process> for a parameter with the given name. The value for
436that parameter will be passed to L</repeat>, to automatically create the
437new copies.
438
439If L</increment_field_names> is true (the default), this is essential: if the
440elements corresponding to the new fieldnames (foo_1, bar_2, etc.) are not
441present on the form during L<HTML::FormFu/process>, no Processors
442(Constraints, etc.) will be run on the fields, and their values will not
443be returned by L<HTML::FormFu/params> or L<HTML::FormFu/param>.
444
445=head2 increment_field_names
446
447Arguments: $bool
448
449Default Value: 1
450
451If true, then all fields will have C<< _n >> appended to their name, where
452C<n> is the L</repeatable_count> value.
453
454=head2 repeatable_count
455
456This is set on each new L<Block|HTML::FormFu::Element::Block> element
457returned by L</repeat>, starting at number C<1>.
458
459Because this is an 'inherited accessor' available on all elements, it can be
460used to determine whether any element is a child of a Repeatable element.
461
462Only available after L<repeat> has been called.
463
464=head2 repeatable_count_no_inherit
465
466A non-inheriting variant of L</repeatable_count>.
467
468=head2 nested_name
469
470If the L</nested_name> attribute is set, the naming scheme of the Repeatable
471element's children is switched to add the counter to the repeatable blocks
472themselves.
473
474    ---
475    elements:
476      - type: Repeatable
477        nested_name: my_rep
478        elements:
479          - name: foo
480          - name: bar
481
482Calling 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
493Because this is an 'inherited accessor' available on all elements, it can be
494used to determine whether any element is a child of a Repeatable element.
495
496=head2 attributes
497
498=head2 attrs
499
500Any 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
511Calling 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
520See L<HTML::FormFu/attributes> for details.
521
522=head2 tag
523
524The 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
534Calling 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
543See L<HTML::FormFu::Element::Block/tag> for details.
544
545=head2 auto_id
546
547As well as the usual substitutions, any instances of C<%r> will be
548replaced with the value of L</repeatable_count>.
549
550See 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
560Calling 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
571Not supported for Repeatable elements - will throw a fatal error if called as
572a setter.
573
574=head1 CAVEATS
575
576=head2 Unsupported Constraints
577
578Note that constraints with an L<others|HTML::FormFu::Role::Constraint::Others>
579method do not work correctly within a Repeatable block. Currently, these are:
580L<AllOrNone|HTML::FormFu::Constraint::AllOrNone>,
581L<DependOn|HTML::FormFu::Constraint::DependOn>,
582L<Equal|HTML::FormFu::Constraint::Equal>,
583L<MinMaxFields|HTML::FormFu::Constraint::MinMaxFields>,
584L<reCAPTCHA|HTML::FormFu::Constraint::reCAPTCHA>.
585Also, the L<CallbackOnce|HTML::FormFu::Constraint::CallbackOnce> constraint
586won't work within a Repeatable block, as it wouldn't make much sense.
587
588=head2 Work-arounds
589
590See L<HTML::FormFu::Filter::ForceListValue> to address a problem with
591L<increment_field_names> disabled, and increading the L<repeat> on the
592server-side.
593
594=head1 SEE ALSO
595
596Is a sub-class of, and inherits methods from
597L<HTML::FormFu::Element::Block>,
598L<HTML::FormFu::Element>
599
600L<HTML::FormFu>
601
602=head1 AUTHOR
603
604Carl Franks, C<cfranks@cpan.org>
605
606=head1 LICENSE
607
608This library is free software, you can redistribute it and/or modify it under
609the same terms as Perl itself.
610
611=head1 AUTHOR
612
613Carl Franks <cpan@fireartist.com>
614
615=head1 COPYRIGHT AND LICENSE
616
617This software is copyright (c) 2018 by Carl Franks.
618
619This is free software; you can redistribute it and/or modify it under
620the same terms as the Perl 5 programming language system itself.
621
622=cut
623