use strict;
package HTML::FormFu::Element::Repeatable;
$HTML::FormFu::Element::Repeatable::VERSION = '2.07';
# ABSTRACT: repeatable block element
use Moose;
use MooseX::Attribute::Chained;
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 => ['Chained'] );
has _original_elements => ( is => 'rw' );
has increment_field_names => (
is => 'rw',
default => 1,
lazy => 1,
traits => ['Chained'],
);
# This attribute is currently not documented as FF::Model::HashRef
# only supports '_'
has repeatable_delimiter => (
is => 'rw',
default => '_',
lazy => 1,
traits => ['Chained'],
);
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__
=pod
=encoding UTF-8
=head1 NAME
HTML::FormFu::Element::Repeatable - repeatable block element
=head1 VERSION
version 2.07
=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 substitutions, 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.
=head1 AUTHOR
Carl Franks <cpan@fireartist.com>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2018 by Carl Franks.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut