package HTML::FormFu::Element::Repeatable; use strict; use base 'HTML::FormFu::Element::Block'; use HTML::FormFu::Util qw( DEBUG_PROCESS debug ); use Class::C3; use List::Util qw( first ); use Carp qw( croak ); __PACKAGE__->mk_item_accessors( qw( _original_elements increment_field_names counter_name repeatable_delimiter ) ); sub new { my $self = shift->next::method(@_); $self->filename('repeatable'); $self->is_repeatable(1); $self->increment_field_names(1); # TODO # This setter is currently not documentes as FF::Model::HashRef # only supports '_' $self->repeatable_delimiter('_'); return $self; } 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); for my $field ( @{ $block->get_fields } ) { map { $_->parent($field) } @{ $field->_deflators }, @{ $field->_filters }, @{ $field->_constraints }, @{ $field->_inflators }, @{ $field->_validators }, @{ $field->_transformers }, @{ $field->_plugins }, ; } my $block_fields = $block->get_fields; my @block_constraints = map { @{ $_->get_constraints } } @$block_fields; # rename any 'others' fields my @others_constraints = grep { defined $_->others } grep { $_->can('others') } @block_constraints; for my $constraint (@others_constraints) { my $others = $constraint->others; if ( !ref $others ) { $others = [$others]; } my @new_others; for my $name (@$others) { my $field = ( first { $_->original_nested_name eq $name } @$block_fields ) || first { $_->original_name eq $name } @$block_fields; if ( defined $field ) { push @new_others, $field->nested_name; } else { push @new_others, $name; } } $constraint->others( \@new_others ); } # rename any 'when' fields my @when_constraints = grep { defined $_->when } @block_constraints; for my $constraint (@when_constraints) { my $when = $constraint->when; my $name = $when->{field}; my $field = first { $_->original_nested_name eq $name } @$block_fields; if ( defined $field ) { $when->{field} = $field->nested_name; } } push @return, $block; } return \@return; } 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); for my $field ( @{ $block->get_fields } ) { map { $_->parent($field) } @{ $field->_deflators }, @{ $field->_filters }, @{ $field->_constraints }, @{ $field->_inflators }, @{ $field->_validators }, @{ $field->_transformers }, @{ $field->_plugins }, ; } my $block_fields = $block->get_fields; my @block_constraints = map { @{ $_->get_constraints } } @$block_fields; # rename any 'others' fields my @others_constraints = grep { defined $_->others } grep { $_->can('others') } @block_constraints; for my $constraint (@others_constraints) { my $others = $constraint->others; if ( !ref $others ) { $others = [$others]; } my @new_others; for my $name (@$others) { my $field = ( first { $_->original_nested_name eq $name } @$block_fields ) || first { $_->original_name eq $name } @$block_fields; if ( defined $field ) { push @new_others, $field->nested_name; } else { push @new_others, $name; } } $constraint->others( \@new_others ); } # rename any 'when' fields my @when_constraints = grep { defined $_->when } @block_constraints; for my $constraint (@when_constraints) { my $when = $constraint->when; my $name = $when->{field}; my $field = first { $_->original_nested_name eq $name } @$block_fields; if ( defined $field ) { $when->{field} = $field->nested_name; } } 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 $form = $self->form; my $count = 1; if ( defined $self->counter_name && defined $form->query ) { my $input = $form->query->param( $self->counter_name ); if ( defined $input && $input =~ /^[1-9][0-9]*\z/ ) { $count = $input; } } if ( !$self->_original_elements ) { DEBUG_PROCESS && debug("calling \$repeatable->count($count)"); $self->repeat($count); } return $self->next::method(@_); } 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; } 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:
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 is called. Although the Repeatable element inherits from L, it doesn't generate a block tag around all the repeated elements - instead it places each repeat of the elements in a new L element, which inherits the Repeatable's display settings, such as L and L. For all constraints attached to fields within a Repeatable block which use either L or L containing names of fields within the same Repeatable block, when L is called, those names will automatically be updated to the new nested-name for each field (taking into account L). =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 automatically to ensure the initial child elements are correctly set up - unless you call L manually first, in which case the child elements you created will be left untouched (otherwise L would overwrite your changes). Any subsequent call to L will delete the previously copied elements before creating new copies - this means you cannot make repeated calls to L within a loop to create more copies. Each copy of the elements returned are contained in a new L element. For example, calling C<< $element->repeat(2) >> on a Repeatable element containing 2 Text fields would return 2 L elements, each containing a copy of the 2 Text fields. =head2 counter_name Arguments: $name If true, the L will be searched during L for a parameter with the given name. The value for that parameter will be passed to L, to automatically create the new copies. If L 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, no Processors (Constraints, etc.) will be run on the fields, and their values will not be returned by L or L. =head2 increment_field_names Arguments: $bool Default Value: 1 If true, then all fields will have C<< _n >> appended to their name, where C is the L value. =head2 repeatable_count This is set on each new L element returned by L, 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. =head2 nested_name If the L attribute is set the naming scheme of the Repeatable elements children is switched to add the counter to the repeatable blocks themself. --- elements: - type: Repeatable nested_name: my_rep elements: - name: foo - name: bar Calling C<< $element->repeat(2) >> would result in the following markup:
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:
See L for details. =head2 tag The L 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: See L for details. =head2 auto_id As well as the usual subtitutions, any instances of C<%r> will be replaced with the value of L. See L 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:
=head2 content Not supported for Repeatable elements - will throw a fatal error if called as a setter. =head1 SEE ALSO Is a sub-class of, and inherits methods from L, L L =head1 AUTHOR Carl Franks, C =head1 LICENSE This library is free software, you can redistribute it and/or modify it under the same terms as Perl itself.