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: