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