File: Repeatable.pm

package info (click to toggle)
libhtml-formfu-perl 2.01000-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 4,116 kB
  • ctags: 828
  • sloc: perl: 12,478; makefile: 7; sql: 5
file content (600 lines) | stat: -rw-r--r-- 16,273 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
package HTML::FormFu::Element::Repeatable;
$HTML::FormFu::Element::Repeatable::VERSION = '2.01';
use Moose;
use MooseX::Attribute::FormFuChained;
extends 'HTML::FormFu::Element::Block';

use HTML::FormFu::Util qw( DEBUG_PROCESS debug );
use List::Util qw( first );
use Carp qw( croak );

has counter_name => ( is => 'rw', traits => ['FormFuChained'] );

has _original_elements => ( is => 'rw' );

has increment_field_names => (
    is      => 'rw',
    default => 1,
    lazy    => 1,
    traits  => ['FormFuChained'],
);

# This attribute is currently not documented as FF::Model::HashRef
# only supports '_'

has repeatable_delimiter => (
    is      => 'rw',
    default => '_',
    lazy    => 1,
    traits  => ['FormFuChained'],
);

after BUILD => sub {
    my $self = shift;

    $self->filename('repeatable');
    $self->is_repeatable(1);

    return;
};

sub repeat {
    my ( $self, $count ) = @_;

    croak "invalid number to repeat"
        if $count !~ /^[0-9]+\z/;

    my $children;

    if ( $self->_original_elements ) {

        # repeat() has already been called
        $children = $self->_original_elements;
    }
    else {
        $children = $self->_elements;

        $self->_original_elements($children);
    }

    croak "no child elements to repeat"
        if !@$children;

    $self->_elements( [] );

    return [] if !$count;

    # switch behaviour
    # If nested_name is set, we add the repeatable counter to the name
    # of the containing block (this repeatable block).
    #     This behaviour eases the creation of client side javascript code
    #     to add and remove repeatable elements client side.
    # If nested_name is *not* set, we add the repeatable counter to the names
    # of the child elements (leaves of the element tree).
    my $nested_name = $self->nested_name;
    if ( defined $nested_name && length $nested_name ) {
        return $self->_repeat_containing_block($count);
    }
    else {
        return $self->_repeat_child_elements($count);
    }
}

sub _repeat_containing_block {
    my ( $self, $count ) = @_;

    my $children = $self->_original_elements;

    # We must not get 'nested.nested_1' instead of 'nested_1' through the
    # nested_name attribute of the Repeatable element, thus we extended
    # FF::Elements::_Field nested_names method to ignore Repeatable elements.
    my $nested_name = $self->nested_name;
    $self->original_nested_name($nested_name);

    # delimiter between nested_name and the incremented counter
    my $delimiter = $self->repeatable_delimiter;

    my @return;

    for my $rep ( 1 .. $count ) {

        # create clones of elements and put them in a new block
        my @clones = map { $_->clone } @$children;
        my $block = $self->element('Block');

        # initiate new block with properties of this repeatable
        $block->_elements( \@clones );
        $block->attributes( $self->attributes );
        $block->tag( $self->tag );

        $block->repeatable_count($rep);

        if ( $self->increment_field_names ) {

            # store the original nested_name attribute for later usage when
            # building the original nested name
            $block->original_nested_name( $block->nested_name )
                if !defined $block->original_nested_name;

            # create new nested name with repeat counter
            $block->nested_name( $nested_name . $delimiter . $rep );

            for my $field ( @{ $block->get_fields } ) {

                if ( defined( my $name = $field->name ) ) {

                    # store original name for later usage when
                    # replacing the field names in constraints
                    $field->original_name($name)
                        if !defined $field->original_name;

                    # store original nested name for later usage when
                    # replacing the field names in constraints
                    $field->original_nested_name(
                        $field->build_original_nested_name )
                        if !defined $field->original_nested_name;
                }
            }
        }

        _reparent_children($block);

        my @fields = @{ $block->get_fields };

        for my $field (@fields) {
            map { $_->parent($field) }
                @{ $field->_deflators },
                @{ $field->_filters },
                @{ $field->_constraints },
                @{ $field->_inflators },
                @{ $field->_validators },
                @{ $field->_transformers },
                @{ $field->_plugins },
                ;
        }

        for my $field (@fields) {
            map { $_->repeatable_repeat( $self, $block ) }
                @{ $field->_constraints };
        }

        push @return, $block;
    }

    return \@return;
}

sub get_field_with_original_name {
    my ( $self, $name, $fields ) = @_;

    my $field = first { $_->original_nested_name eq $name }
    grep { defined $_->original_nested_name } @$fields;

    $field ||= first { $_->original_name eq $name }
    grep { defined $_->original_name } @$fields;

    return $field;
}

sub _repeat_child_elements {
    my ( $self, $count ) = @_;

    my $children = $self->_original_elements;

    # delimiter between nested_name and the incremented counter
    my $delimiter = $self->repeatable_delimiter;

    my @return;

    for my $rep ( 1 .. $count ) {
        my @clones = map { $_->clone } @$children;
        my $block = $self->element('Block');

        $block->_elements( \@clones );
        $block->attributes( $self->attributes );
        $block->tag( $self->tag );

        $block->repeatable_count($rep);

        if ( $self->increment_field_names ) {
            for my $field ( @{ $block->get_fields } ) {

                if ( defined( my $name = $field->name ) ) {
                    $field->original_name($name)
                        if !defined $field->original_name;

                    $field->original_nested_name( $field->nested_name )
                        if !defined $field->original_nested_name;

                    $field->name( ${name} . $delimiter . $rep );
                }
            }
        }

        _reparent_children($block);

        my @fields = @{ $block->get_fields };

        for my $field (@fields) {
            map { $_->parent($field) }
                @{ $field->_deflators },
                @{ $field->_filters },
                @{ $field->_constraints },
                @{ $field->_inflators },
                @{ $field->_validators },
                @{ $field->_transformers },
                @{ $field->_plugins },
                ;
        }

        for my $field (@fields) {
            map { $_->repeatable_repeat( $self, $block ) }
                @{ $field->_constraints };
        }

        push @return, $block;
    }

    return \@return;
}

sub _reparent_children {
    my $self = shift;

    return if !$self->is_block;

    for my $child ( @{ $self->get_elements } ) {
        $child->parent($self);

        _reparent_children($child);
    }
}

sub process {
    my $self = shift;

    my $counter_name = $self->counter_name;
    my $form         = $self->form;
    my $count        = 1;

    if ( defined $counter_name && defined $form->query ) {

        # are we in a nested-repeatable?
        my $parent = $self;

        while ( defined( $parent = $parent->parent ) ) {
            my $field
                = $parent->get_field( { original_name => $counter_name } );

            if ( defined $field ) {
                $counter_name = $field->nested_name;
                last;
            }
        }

        my $input = $form->query->param($counter_name);

        if ( defined $input && $input =~ /^[1-9][0-9]*\z/ ) {
            $count = $input;
        }
    }

    if ( !$self->_original_elements ) {
        DEBUG_PROCESS && debug("calling \$repeatable->repeat($count)");

        $self->repeat($count);
    }

    return $self->SUPER::process(@_);
}

sub content {
    my $self = shift;

    croak "Repeatable elements do not support the content() method"
        if @_;

    return;
}

sub string {
    my ( $self, $args ) = @_;

    $args ||= {};

    my $render
        = exists $args->{render_data}
        ? $args->{render_data}
        : $self->render_data_non_recursive;

    # block template

    my @divs = map { $_->render } @{ $self->get_elements };

    my $html = join "\n", @divs;

    return $html;
}

__PACKAGE__->meta->make_immutable;

1;

__END__

=head1 NAME

HTML::FormFu::Element::Repeatable - repeatable block element

=head1 SYNOPSIS

    ---
    elements:
      - type: Repeatable
        name: my_rep
        elements:
          - name: foo
          - name: bar

Calling C<< $element->repeat(2) >> would result in the following markup:

    <div>
        <input name="my_rep.foo_1" type="text" />
        <input name="my_rep.bar_1" type="text" />
    </div>
    <div>
        <input name="my_rep.foo_2" type="text" />
        <input name="my_rep.bar_2" type="text" />
    </div>

Example of constraints:

    ----
    elements:
      - type: Repeatable
        name: my_rep
        elements:
          - name: id

          - name: foo
            constraints:
              - type: Required
                when:
                  field: 'my_rep.id' # use full nested-name

          - name: bar
            constraints:
              - type: Equal
                others: 'my_rep.foo' # use full nested-name

=head1 DESCRIPTION

Provides a way to extend a form at run-time, by copying and repeating its
child elements.

The elements intended for copying must be added before L</repeat> is called.

Although the Repeatable element inherits from
L<Block|HTML::FormFu::Element::Block>, it doesn't generate a block tag
around all the repeated elements - instead it places each repeat of the
elements in a new L<Block|HTML::FormFu::Element::Block> element, which
inherits the Repeatable's display settings, such as L</attributes> and
L</tag>.

For all constraints attached to fields within a Repeatable block which use
either L<others|HTML::FormFu::Role::Constraint::Others/others> or
L<when|HTML::FormFu::Constraint/when> containing names of fields within
the same Repeatable block, when L<repeat> is called, those names will
automatically be updated to the new nested-name for each field (taking
into account L<increment_field_names>).

=head1 METHODS

=head2 repeat

Arguments: [$count]

Return Value: $arrayref_of_new_child_blocks

This method creates C<$count> number of copies of the child elements.
If no argument C<$count> is provided, it defaults to C<1>.

Note that C<< $form->process >> will call L</repeat> automatically to ensure the
initial child elements are correctly set up - unless you call L</repeat>
manually first, in which case the child elements you created will be left
untouched (otherwise L<process|HTML::FormFu/process> would overwrite your
changes).

Any subsequent call to L</repeat> will delete the previously copied elements
before creating new copies - this means you cannot make repeated calls to
L</repeat> within a loop to create more copies.

Each copy of the elements returned are contained in a new
L<Block|HTML::FormFu::Element::Block> element. For example, calling
C<< $element->repeat(2) >> on a Repeatable element containing 2 Text fields
would return 2 L<Block|HTML::FormFu::Element::Block> elements, each
containing a copy of the 2 Text fields.

=head2 counter_name

Arguments: $name

If true, the L<HTML::FormFu/query> will be searched during
L<HTML::FormFu/process> for a parameter with the given name. The value for
that parameter will be passed to L</repeat>, to automatically create the
new copies.

If L</increment_field_names> is true (the default), this is essential: if the
elements corresponding to the new fieldnames (foo_1, bar_2, etc.) are not
present on the form during L<HTML::FormFu/process>, no Processors
(Constraints, etc.) will be run on the fields, and their values will not
be returned by L<HTML::FormFu/params> or L<HTML::FormFu/param>.

=head2 increment_field_names

Arguments: $bool

Default Value: 1

If true, then all fields will have C<< _n >> appended to their name, where
C<n> is the L</repeatable_count> value.

=head2 repeatable_count

This is set on each new L<Block|HTML::FormFu::Element::Block> element
returned by L</repeat>, starting at number C<1>.

Because this is an 'inherited accessor' available on all elements, it can be
used to determine whether any element is a child of a Repeatable element.

Only available after L<repeat> has been called.

=head2 repeatable_count_no_inherit

A non-inheriting variant of L</repeatable_count>.

=head2 nested_name

If the L</nested_name> attribute is set, the naming scheme of the Repeatable
element's children is switched to add the counter to the repeatable blocks
themselves.

    ---
    elements:
      - type: Repeatable
        nested_name: my_rep
        elements:
          - name: foo
          - name: bar

Calling C<< $element->repeat(2) >> would result in the following markup:

    <div>
        <input name="my_rep_1.foo" type="text" />
        <input name="my_rep_1.bar" type="text" />
    </div>
    <div>
        <input name="my_rep_2.foo" type="text" />
        <input name="my_rep_2.bar" type="text" />
    </div>


Because this is an 'inherited accessor' available on all elements, it can be
used to determine whether any element is a child of a Repeatable element.

=head2 attributes

=head2 attrs

Any attributes set will be passed to every repeated Block of elements.

    ---
    elements:
      - type: Repeatable
        name: my_rep
        attributes:
          class: rep
        elements:
          - name: foo

Calling C<< $element->repeat(2) >> would result in the following markup:

    <div class="rep">
        <input name="my_rep.foo_1" type="text" />
    </div>
    <div class="rep">
        <input name="my_rep.foo_2" type="text" />
    </div>

See L<HTML::FormFu/attributes> for details.

=head2 tag

The L</tag> value will be passed to every repeated Block of elements.

    ---
    elements:
      - type: Repeatable
        name: my_rep
        tag: span
        elements:
          - name: foo

Calling C<< $element->repeat(2) >> would result in the following markup:

    <span>
        <input name="my_rep.foo_1" type="text" />
    </span>
    <span>
        <input name="my_rep.foo_2" type="text" />
    </span>

See L<HTML::FormFu::Element::Block/tag> for details.

=head2 auto_id

As well as the usual subtitutions, any instances of C<%r> will be
replaced with the value of L</repeatable_count>.

See L<HTML::FormFu::Element::Block/auto_id> for further details.

    ---
    elements:
      - type: Repeatable
        name: my_rep
        auto_id: "%n_%r"
        elements:
          - name: foo

Calling C<< $element->repeat(2) >> would result in the following markup:

    <div>
        <input name="my_rep.foo_1" id="foo_1" type="text" />
    </div>
    <div>
        <input name="my_rep.foo_2" id="foo_2" type="text" />
    </div>

=head2 content

Not supported for Repeatable elements - will throw a fatal error if called as
a setter.

=head1 CAVEATS

=head2 Unsupported Constraints

Note that constraints with an L<others|HTML::FormFu::Role::Constraint::Others> 
method do not work correctly within a Repeatable block. Currently, these are:
L<AllOrNone|HTML::FormFu::Constraint::AllOrNone>, 
L<DependOn|HTML::FormFu::Constraint::DependOn>, 
L<Equal|HTML::FormFu::Constraint::Equal>, 
L<MinMaxFields|HTML::FormFu::Constraint::MinMaxFields>, 
L<reCAPTCHA|HTML::FormFu::Constraint::reCAPTCHA>.
Also, the L<CallbackOnce|HTML::FormFu::Constraint::CallbackOnce> constraint
won't work within a Repeatable block, as it wouldn't make much sense.

=head2 Work-arounds

See L<HTML::FormFu::Filter::ForceListValue> to address a problem with 
L<increment_field_names> disabled, and increading the L<repeat> on the
server-side.

=head1 SEE ALSO

Is a sub-class of, and inherits methods from
L<HTML::FormFu::Element::Block>,
L<HTML::FormFu::Element>

L<HTML::FormFu>

=head1 AUTHOR

Carl Franks, C<cfranks@cpan.org>

=head1 LICENSE

This library is free software, you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut