Skip to content

Commit a42d84b

Browse files
committed
Improved the way of building names for repeatable elements.
For repeatable elements that don't have a nested_name attribute we still use the old behaviour. The repeatable counter is added to the field names (e. g. 'field_1_1' for two nested repeatable elements without nested_name attributes). If a nested_name attribute is given, we now add the counter to the repeatable name part to ease the automatic generation of fields with client side technologies like javascript. So now we get 'outer_1.inner_1.field' for two nested repeatable elements with the nested_name attributes 'outer' and 'inner'.
1 parent 9431615 commit a42d84b

15 files changed

+415
-108
lines changed

lib/HTML/FormFu/Element/Block.pm

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use List::MoreUtils qw( uniq );
1010
use Storable qw( dclone );
1111
use Carp qw( croak );
1212

13-
__PACKAGE__->mk_item_accessors(qw( tag _elements nested_name ));
13+
__PACKAGE__->mk_item_accessors( qw(
14+
tag _elements
15+
nested_name original_nested_name
16+
) );
1417

1518
__PACKAGE__->mk_output_accessors(qw( content ));
1619

lib/HTML/FormFu/Element/Repeatable.pm

+181-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ __PACKAGE__->mk_item_accessors( qw(
1212
_original_elements
1313
increment_field_names
1414
counter_name
15+
repeatable_delimiter
1516
) );
1617

1718
sub new {
@@ -20,6 +21,10 @@ sub new {
2021
$self->filename('repeatable');
2122
$self->is_repeatable(1);
2223
$self->increment_field_names(1);
24+
# TODO
25+
# This setter is currently not documentes as FF::Model::HashRef
26+
# only supports '_'
27+
$self->repeatable_delimiter('_');
2328

2429
return $self;
2530
}
@@ -49,7 +54,152 @@ sub repeat {
4954
$self->_elements( [] );
5055

5156
return [] if !$count;
52-
57+
58+
# switch behaviour
59+
# If nested_name is set, we add the repeatable counter to the name
60+
# of the containing block (this repeatable block).
61+
# This behaviour eases the creation of client side javascript code
62+
# to add and remove repeatable elements client side.
63+
# If nested_name is *not* set, we add the repeatable counter to the names
64+
# of the child elements (leaves of the element tree).
65+
my $nested_name = $self->nested_name;
66+
if (defined $nested_name && length $nested_name) {
67+
return $self->_repeat_containing_block( $count );
68+
}
69+
else {
70+
return $self->_repeat_child_elements( $count );
71+
}
72+
}
73+
74+
sub _repeat_containing_block {
75+
my ( $self, $count ) = @_;
76+
77+
my $children = $self->_original_elements;
78+
79+
# We must not get 'nested.nested_1' instead of 'nested_1' through the
80+
# nested_name attribute of the Repeatable element, thus we extended
81+
# FF::Elements::_Field nested_names method to ignore Repeatable elements.
82+
my $nested_name = $self->nested_name;
83+
$self->original_nested_name( $nested_name );
84+
85+
# delimiter between nested_name and the incremented counter
86+
my $delimiter = $self->repeatable_delimiter;
87+
88+
my @return;
89+
90+
for my $rep ( 1 .. $count ) {
91+
# create clones of elements and put them in a new block
92+
my @clones = map { $_->clone } @$children;
93+
my $block = $self->element('Block');
94+
95+
# initiate new block with properties of this repeatable
96+
$block->_elements( \@clones );
97+
$block->attributes( $self->attributes );
98+
$block->tag( $self->tag );
99+
100+
$block->repeatable_count($rep);
101+
102+
if ( $self->increment_field_names ) {
103+
# store the original nested_name attribute for later usage when
104+
# building the original nested name
105+
$block->original_nested_name( $block->nested_name )
106+
if !defined $block->original_nested_name;
107+
108+
# create new nested name with repeat counter
109+
$block->nested_name( $nested_name . $delimiter . $rep );
110+
111+
for my $field ( @{ $block->get_fields } ) {
112+
113+
if ( defined( my $name = $field->name ) ) {
114+
# store original name for later usage when
115+
# replacing the field names in constraints
116+
$field->original_name($name)
117+
if !defined $field->original_name;
118+
119+
# store original nested name for later usage when
120+
# replacing the field names in constraints
121+
$field->original_nested_name( $field->build_original_nested_name )
122+
if !defined $field->original_nested_name;
123+
}
124+
}
125+
}
126+
127+
_reparent_children($block);
128+
129+
for my $field ( @{ $block->get_fields } ) {
130+
map { $_->parent($field) }
131+
@{ $field->_deflators },
132+
@{ $field->_filters },
133+
@{ $field->_constraints },
134+
@{ $field->_inflators },
135+
@{ $field->_validators },
136+
@{ $field->_transformers },
137+
@{ $field->_plugins },
138+
;
139+
}
140+
141+
my $block_fields = $block->get_fields;
142+
143+
my @block_constraints = map { @{ $_->get_constraints } } @$block_fields;
144+
145+
# rename any 'others' fields
146+
my @others_constraints = grep { defined $_->others }
147+
grep { $_->can('others') } @block_constraints;
148+
149+
for my $constraint (@others_constraints) {
150+
my $others = $constraint->others;
151+
if ( !ref $others ) {
152+
$others = [$others];
153+
}
154+
my @new_others;
155+
156+
for my $name (@$others) {
157+
my $field
158+
= ( first { $_->original_nested_name eq $name }
159+
@$block_fields )
160+
|| first { $_->original_name eq $name } @$block_fields;
161+
162+
if ( defined $field ) {
163+
push @new_others, $field->nested_name;
164+
}
165+
else {
166+
push @new_others, $name;
167+
}
168+
}
169+
170+
$constraint->others( \@new_others );
171+
}
172+
173+
# rename any 'when' fields
174+
my @when_constraints = grep { defined $_->when } @block_constraints;
175+
176+
for my $constraint (@when_constraints) {
177+
my $when = $constraint->when;
178+
my $name = $when->{field};
179+
180+
my $field
181+
= first { $_->original_nested_name eq $name } @$block_fields;
182+
183+
if ( defined $field ) {
184+
$when->{field} = $field->nested_name;
185+
}
186+
}
187+
188+
push @return, $block;
189+
190+
}
191+
192+
return \@return;
193+
}
194+
195+
sub _repeat_child_elements {
196+
my ( $self, $count ) = @_;
197+
198+
my $children = $self->_original_elements;
199+
200+
# delimiter between nested_name and the incremented counter
201+
my $delimiter = $self->repeatable_delimiter;
202+
53203
my @return;
54204

55205
for my $rep ( 1 .. $count ) {
@@ -72,7 +222,7 @@ sub repeat {
72222
$field->original_nested_name( $field->nested_name )
73223
if !defined $field->original_nested_name;
74224

75-
$field->name("${name}_$rep");
225+
$field->name(${name} . $delimiter . $rep);
76226
}
77227
}
78228
}
@@ -333,6 +483,35 @@ C<n> is the L</repeatable_count> value.
333483
This is set on each new L<Block|HTML::FormFu::Element::Block> element
334484
returned by L</repeat>, starting at number C<1>.
335485
486+
Because this is an 'inherited accessor' available on all elements, it can be
487+
used to determine whether any element is a child of a Repeatable element.
488+
489+
=head2 nested_name
490+
491+
If the L</nested_name> attribute is set the naming scheme of the Repeatable
492+
elements children is switched to add the counter to the repeatable blocks
493+
themself.
494+
495+
---
496+
elements:
497+
- type: Repeatable
498+
nested_name: my_rep
499+
elements:
500+
- name: foo
501+
- name: bar
502+
503+
Calling C<< $element->repeat(2) >> would result in the following markup:
504+
505+
<div>
506+
<input name="my_rep_1.foo" type="text" />
507+
<input name="my_rep_1.bar" type="text" />
508+
</div>
509+
<div>
510+
<input name="myrep_2.foo" type="text" />
511+
<input name="myrep_2.bar" type="text" />
512+
</div>
513+
514+
336515
Because this is an 'inherited accessor' available on all elements, it can be
337516
used to determine whether any element is a child of a Repeatable element.
338517

lib/HTML/FormFu/Element/_Field.pm

+88
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ sub nested_name {
132132
if ( $self->form->nested_subscript ) {
133133
my $name = shift @names;
134134
map { $name .= "[$_]" } @names;
135+
# TODO - Mario Minati 19.05.2009
136+
# Does this (name formatted as '[name]') collide with FF::Model::HashRef as
137+
# it uses /_\d/ to parse repeatable names?
135138
return $name;
136139
}
137140
else {
@@ -153,10 +156,17 @@ sub nested_names {
153156
while ( defined( $parent = $parent->{parent} ) ) {
154157

155158
if ( $parent->can('is_field') && $parent->is_field ) {
159+
# handling Field
156160
push @names, $parent->name
157161
if defined $parent->name;
158162
}
163+
elsif ( $parent->can('is_repeatable') && $parent->is_repeatable ) {
164+
# handling Repeatable
165+
# ignore Repeatables nested_name attribute as it is provided
166+
# by the childrens Block elements
167+
}
159168
else {
169+
# handling 'not Field' and 'not Repeatable'
160170
push @names, $parent->nested_name
161171
if defined $parent->nested_name;
162172
}
@@ -170,6 +180,84 @@ sub nested_names {
170180
return ( $self->name );
171181
}
172182

183+
sub build_original_nested_name {
184+
my ($self) = @_;
185+
186+
croak 'cannot set build_original_nested_name' if @_ > 1;
187+
188+
return if !defined $self->name;
189+
190+
my @names = $self->build_original_nested_names;
191+
192+
if ( $self->form->nested_subscript ) {
193+
my $name = shift @names;
194+
map { $name .= "[$_]" } @names;
195+
# TODO - Mario Minati 19.05.2009
196+
# Does this (name formatted as '[name]') collide with FF::Model::HashRef as
197+
# it uses /_\d/ to parse repeatable names?
198+
return $name;
199+
}
200+
else {
201+
return join ".", @names;
202+
}
203+
}
204+
205+
sub build_original_nested_names {
206+
my ($self) = @_;
207+
208+
croak 'cannot set build_original_nested_names' if @_ > 1;
209+
210+
# TODO - Mario Minati 19.05.2009
211+
# Maybe we have to use original_name instead of name.
212+
# Yet there is no testcase, which is currently failing.
213+
214+
if ( defined( my $name = $self->name ) ) {
215+
my @names;
216+
my $parent = $self;
217+
218+
# micro optimization! this method's called a lot, so access
219+
# parent hashkey directly, instead of calling parent()
220+
while ( defined( $parent = $parent->{parent} ) ) {
221+
222+
if ( $parent->can('is_field') && $parent->is_field ) {
223+
# handling Field
224+
if (defined $parent->original_name) {
225+
push @names, $parent->original_name;
226+
}
227+
elsif (defined $parent->name) {
228+
push @names, $parent->name;
229+
}
230+
}
231+
elsif ( $parent->can('is_repeatable') && $parent->is_repeatable ) {
232+
# handling Repeatable
233+
# TODO - Mario Minati 19.05.2009
234+
# Do we have to take care of chains of Repeatable elements, if the Block
235+
# elements have already been created for the outer Repeatable elements to
236+
# avoid 'outer.outer_1.inner'
237+
# Yet there is no failing testcase. All testcases in FF and FF::Model::DBIC
238+
# which have nested repeatable elements are passing currently.
239+
push @names, $parent->original_nested_name
240+
if defined $parent->original_nested_name;
241+
}
242+
else {
243+
# handling 'not Field' and 'not Repeatable'
244+
if ($parent->can('original_nested_name') && defined $parent->original_nested_name) {
245+
push @names, $parent->original_nested_name;
246+
}
247+
elsif (defined $parent->nested_name) {
248+
push @names, $parent->nested_name
249+
}
250+
}
251+
}
252+
253+
if (@names) {
254+
return reverse $name, @names;
255+
}
256+
}
257+
258+
return ( $self->name );
259+
}
260+
173261
sub nested_base {
174262
my ($self) = @_;
175263

lib/HTML/FormFu/Element/_MultiElement.pm

+7
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ sub nested_names {
1717
while ( defined( $parent = $parent->parent ) ) {
1818

1919
if ( $parent->can('is_field') && $parent->is_field ) {
20+
# handling Field
2021
push @names, $parent->name
2122
if defined $parent->name;
2223
}
24+
elsif ( $parent->can('is_repeatable') && $parent->is_repeatable ) {
25+
# handling Repeatable
26+
# ignore Repeatables nested_name attribute as it is provided
27+
# by the childrens Block elements
28+
}
2329
else {
30+
# handling 'not Field' and 'not Repeatable'
2431
push @names, $parent->nested_name
2532
if defined $parent->nested_name;
2633
}

0 commit comments

Comments
 (0)