Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/HTML/FormFu.pm
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ has form_error_message_class => (
lazy => 1,
);

has repeatable_max_counter => (
is => 'rw',
default => 100,
lazy => 1,
traits => ['Chained'],
);
Comment on lines +114 to +119

our @MULTIFORM_SHARED = ( qw(
javascript
javascript_src
Expand Down Expand Up @@ -1485,6 +1492,19 @@ used as the return value for L</submitted>.
If L</indicator> is not set, L</submitted> will return true if a value for
any known fieldname was submitted.

=head2 repeatable_max_counter

Arguments: $number

Default Value: C<100>

The default L<max_counter|HTML::FormFu::Element::Repeatable/max_counter>
for all L<Repeatable|HTML::FormFu::Element::Repeatable> elements in this
form. Individual Repeatable elements can override this by setting their own
L<max_counter|HTML::FormFu::Element::Repeatable/max_counter>.

Set to C<0> to disable clamping form-wide.

=head2 auto_fieldset

Arguments: 1
Expand Down
54 changes: 53 additions & 1 deletion lib/HTML/FormFu/Element/Repeatable.pm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ use Carp qw( croak );

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

has max_counter => (
is => 'rw',
default => sub {
my $form = $_[0]->form;
return $form->repeatable_max_counter if $form;
return 100;
},
lazy => 1,
traits => ['Chained'],
);
Comment on lines +17 to +26

has counter_clamped => (
is => 'ro',
default => 0,
writer => '_set_counter_clamped',
);

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

has increment_field_names => (
Expand Down Expand Up @@ -261,6 +278,8 @@ sub process {
my $form = $self->form;
my $count = 1;

$self->_set_counter_clamped(0);

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

# are we in a nested-repeatable?
Expand All @@ -279,7 +298,15 @@ sub process {
my $input = $form->query->param($counter_name);

if ( defined $input && $input =~ /^[1-9][0-9]*\z/ ) {
$count = $input;
my $max = $self->max_counter;

if ( defined $max && $max > 0 && $input > $max ) {
$count = $max;
$self->_set_counter_clamped(1);
}
else {
$count = $input;
}
}
}

Expand Down Expand Up @@ -426,6 +453,31 @@ 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 max_counter

Arguments: $number

Default Value: C<100>

The largest repeat count that will be accepted from the L</counter_name>
query parameter. A larger client-supplied value is clamped to this number.

The default is inherited from L<HTML::FormFu/repeatable_max_counter>, so
setting that on the form sets the default for all its Repeatable elements.

Set to C<0> to accept any client-supplied count without clamping. Doing so
lets a single request drive an unbounded number of element clones.

Calling L</repeat> from application code is not affected by this setting,
so a form repeated once per database row is unaffected however many rows
there are.

=head2 counter_clamped

Read-only boolean. True if the most recent L<HTML::FormFu/process> call
clamped the client-supplied count to L</max_counter>. Reset to false at the
start of each L<HTML::FormFu/process> call.

=head2 increment_field_names

Arguments: $bool
Expand Down
123 changes: 123 additions & 0 deletions t/repeatable/max_counter.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use strict;
use warnings;

use Test::More tests => 13;

use HTML::FormFu;
use lib 't/lib';
use HTMLFormFu::TestLib;

# Test that max_counter defaults to 100 and clamps large values
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' } } );

$form->load_config_file('t/repeatable/max_counter.yml');

my $repeatable = $form->get_element( { type => 'Repeatable' } );

is( $repeatable->max_counter, 100, 'default max_counter is 100' );

$form->process( { count => 200 } );

my @blocks = @{ $repeatable->get_elements };
is( scalar @blocks, 100, '200 repeats clamped to 100' );
ok( $repeatable->counter_clamped, 'counter_clamped is true after clamping' );
}

# Test that max_counter can be set lower
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' } } );

$form->load_config_file('t/repeatable/max_counter.yml');

my $repeatable = $form->get_element( { type => 'Repeatable' } );
$repeatable->max_counter(5);

$form->process( { count => 10 } );

my @blocks = @{ $repeatable->get_elements };
is( scalar @blocks, 5, '10 repeats clamped to 5' );
ok( $repeatable->counter_clamped, 'counter_clamped is true' );
}

# Test that max_counter=0 means unlimited (not clamped)
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' } } );

$form->load_config_file('t/repeatable/max_counter.yml');

my $repeatable = $form->get_element( { type => 'Repeatable' } );
$repeatable->max_counter(0);

$form->process( { count => 3 } );

my @blocks = @{ $repeatable->get_elements };
is( scalar @blocks, 3, 'max_counter=0 allows 3 repeats unclamped' );
ok( !$repeatable->counter_clamped, 'counter_clamped is false' );
}

# Test that normal small values are unaffected
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' } } );

$form->load_config_file('t/repeatable/max_counter.yml');

$form->process( { count => 3 } );

my $repeatable = $form->get_element( { type => 'Repeatable' } );
my @blocks = @{ $repeatable->get_elements };
is( scalar @blocks, 3, '3 repeats below default max_counter' );
ok( !$repeatable->counter_clamped, 'counter_clamped is false' );
}

# Test that calling repeat() directly is unaffected by max_counter
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' } } );

$form->load_config_file('t/repeatable/max_counter.yml');

my $repeatable = $form->get_element( { type => 'Repeatable' } );
$repeatable->repeat(200);

my @blocks = @{ $repeatable->get_elements };
is( scalar @blocks, 200, 'direct repeat(200) unaffected by max_counter' );
}

# Test form-level repeatable_max_counter
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' },
repeatable_max_counter => 50,
} );

$form->load_config_file('t/repeatable/max_counter.yml');

my $repeatable = $form->get_element( { type => 'Repeatable' } );

is( $repeatable->max_counter, 50,
'max_counter inherits form repeatable_max_counter' );

$form->process( { count => 200 } );

my @blocks = @{ $repeatable->get_elements };
is( scalar @blocks, 50, '200 clamped to form-level 50' );
}

# Test chained method works
{
my $form = HTML::FormFu->new(
{ tt_args => { INCLUDE_PATH => 'share/templates/tt/xhtml' } } );

$form->load_config_file('t/repeatable/max_counter.yml');

my $repeatable = $form->get_element( { type => 'Repeatable' } );

$repeatable->max_counter(10)->max_counter(20);

is( $repeatable->max_counter, 20, 'chained max_counter setter works' );
}
11 changes: 11 additions & 0 deletions t/repeatable/max_counter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
elements:
- type: Hidden
name: count

- type: Repeatable
nested_name: rep
increment_field_names: 1
counter_name: count
elements:
- name: foo
Loading