File: Repeatable.pm

package info (click to toggle)
libhtml-formhandler-perl 0.40057-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 2,320 kB
  • ctags: 685
  • sloc: perl: 8,849; makefile: 2
file content (443 lines) | stat: -rw-r--r-- 14,106 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
package HTML::FormHandler::Field::Repeatable;
# ABSTRACT: repeatable (array) field

use Moose;
extends 'HTML::FormHandler::Field::Compound';

use aliased 'HTML::FormHandler::Field::Repeatable::Instance';
use HTML::FormHandler::Field::PrimaryKey;
use HTML::FormHandler::Merge ('merge');
use Data::Clone ('data_clone');


has 'contains' => (
    isa       => 'HTML::FormHandler::Field',
    is        => 'rw',
    predicate => 'has_contains',
);

has 'init_contains' => ( is => 'rw', isa => 'HashRef', traits => ['Hash'],
    default => sub {{}},
    handles => { has_init_contains => 'count' },
);

has 'num_when_empty' => ( isa => 'Int',  is => 'rw', default => 1 );
has 'num_extra'      => ( isa => 'Int',  is => 'rw', default => 0 );
has 'setup_for_js'   => ( isa => 'Bool', is => 'rw' );
has 'index'          => ( isa => 'Int',  is => 'rw', default => 0 );
has 'auto_id'        => ( isa => 'Bool', is => 'rw', default => 0 );
has 'is_repeatable'  => ( isa => 'Bool', is => 'ro', default => 1 );
has '+widget'        => ( default => 'Repeatable' );

sub _fields_validate {
    my $self = shift;
    # loop through array of fields and validate
    my @value_array;
    foreach my $field ( $self->all_fields ) {
        next if ( $field->is_inactive );
        # Validate each field and "inflate" input -> value.
        $field->validate_field;    # this calls the field's 'validate' routine
        push @value_array, $field->value if $field->has_value;
    }
    $self->_set_value( \@value_array );
}

sub init_state {
    my $self = shift;

    # must clear out instances built last time
    unless ( $self->has_contains ) {
        if ( $self->num_fields == 1 && $self->field('contains') ) {
            $self->field('contains')->is_contains(1);
            $self->contains( $self->field('contains') );
        }
        else {
            $self->contains( $self->create_element );
        }
    }
    $self->clear_fields;
}

sub create_element {
    my ($self) = @_;

    my $instance;
    my $instance_attr = {
        name   => 'contains',
        parent => $self,
        type   => 'Repeatable::Instance',
        is_contains => 1,
    };
    # primary_key array is used for reloading after database update
    $instance_attr->{primary_key} = $self->primary_key
        if $self->has_primary_key;
    if( $self->has_init_contains ) {
        $instance_attr = merge( $self->init_contains, $instance_attr );
    }
    if( $self->form ) {
        $instance_attr->{form} = $self->form;
        $instance = $self->form->_make_adhoc_field(
            'HTML::FormHandler::Field::Repeatable::Instance',
            $instance_attr );
    }
    else {
        $instance = Instance->new( %$instance_attr );
    }
    # copy the fields from this field into the instance
    $instance->add_field( $self->all_fields );
    foreach my $fld ( $instance->all_fields ) {
        $fld->parent($instance);
    }

    # set required flag
    $instance->required( $self->required );

    # auto_id has no way to change widgets...deprecate this?
    if ( $self->auto_id ) {
        unless ( grep $_->can('is_primary_key') && $_->is_primary_key, $instance->all_fields ) {
            my $field;
            my $field_attr = { name => 'id', parent => $instance };
            if ( $self->form ) { # this will pull in the widget role
                $field_attr->{form} = $self->form;
                $field = $self->form->_make_adhoc_field(
                    'HTML::FormHandler::Field::PrimaryKey', $field_attr );
            }
            else { # the following won't have a widget role applied
                $field = HTML::FormHandler::Field::PrimaryKey->new( %$field_attr );
            }
            $instance->add_field($field);
        }
    }
    $_->parent($instance) for $instance->all_fields;
    return $instance;
}

sub clone_element {
    my ( $self, $index ) = @_;

    my $field = $self->contains->clone( errors => [], error_fields => [] );
    $field->name($index);
    $field->parent($self);
    if ( $field->has_fields ) {
        $self->clone_fields( $field, [ $field->all_fields ] );
    }
    return $field;
}

sub clone_fields {
    my ( $self, $parent, $fields ) = @_;
    my @field_array;
    $parent->fields( [] );
    foreach my $field ( @{$fields} ) {
        my $new_field = $field->clone( errors => [], error_fields => [] );
        if ( $new_field->has_fields ) {
            $self->clone_fields( $new_field, [ $new_field->all_fields ] );
        }
        $new_field->parent($parent);
        $parent->add_field($new_field);
    }
}

# params exist and validation will be performed (later)
sub _result_from_input {
    my ( $self, $result, $input ) = @_;

    $self->init_state;
    $result->_set_input($input);
    $self->_set_result($result);
    # if Repeatable has array input, need to build instances
    $self->fields( [] );
    my $index = 0;
    if ( ref $input eq 'ARRAY' ) {
        # build appropriate instance array
        foreach my $element ( @{$input} ) {
            next if not defined $element; # skip empty slots
            my $field  = $self->clone_element($index);
            my $result = HTML::FormHandler::Field::Result->new(
                name   => $index,
                parent => $self->result
            );
            $result = $field->_result_from_input( $result, $element, 1 );
            $self->result->add_result($result);
            $self->add_field($field);
            $index++;
        }
    }
    $self->index($index);
    $self->_setup_for_js if $self->setup_for_js;
    $self->result->_set_field_def($self);
    return $self->result;
}

sub _setup_for_js {
    my $self = shift;
    return unless $self->form;
    my $full_name = $self->full_name;
    my $index_level =()= $full_name =~ /{index\d+}/g;
    $index_level++;
    my $field_name = "{index-$index_level}";
    my $field = $self->_add_extra($field_name);
    my $rendered = $field->render;
    # remove extra result & field, now that it's rendered
    $self->result->_pop_result;
    $self->_pop_field;
    # set the information in the form
    # $self->index is the index of the next instance
    $self->form->set_for_js( $self->full_name,
        { index => $self->index, html => $rendered, level => $index_level } );
}

# this is called when there is an init_object or a db item with values
sub _result_from_object {
    my ( $self, $result, $values ) = @_;

    return $self->_result_from_fields($result)
        if ( $self->num_when_empty > 0 && !$values );
    $self->item($values);
    $self->init_state;
    $self->_set_result($result);
    # Create field instances and fill with values
    my $index = 0;
    my @new_values;
    $self->fields( [] );
    $values = [$values] if ( $values && ref $values ne 'ARRAY' );
    foreach my $element ( @{$values} ) {
        next unless $element;
        my $field = $self->clone_element($index);
        my $result =
            HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
        if( $field->has_inflate_default_method ) {
            $element = $field->inflate_default($element);
        }
        $result = $field->_result_from_object( $result, $element );
        push @new_values, $result->value;
        $self->add_field($field);
        $self->result->add_result( $field->result );
        $index++;
    }
    if( my $num_extra = $self->num_extra ) {
        while ($num_extra ) {
            $self->_add_extra($index);
            $num_extra--;
            $index++;
        }
    }
    $self->index($index);
    $self->_setup_for_js if $self->setup_for_js;
    $values = \@new_values if scalar @new_values;
    $self->_set_value($values);
    $self->result->_set_field_def($self);
    return $self->result;
}

sub _add_extra {
    my ($self, $index) = @_;

    my $field = $self->clone_element($index);
    my $result =
        HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
    $result = $field->_result_from_fields($result);
    $self->result->add_result($result) if $result;
    $self->add_field($field);
    return $field;
}

sub add_extra {
    my ( $self, $count ) = @_;
    $count = 1 if not defined $count;
    my $index = $self->index;
    while ( $count ) {
        $self->_add_extra($index);
        $count--;
        $index++;
    }
    $self->index($index);
}

# create an empty field
sub _result_from_fields {
    my ( $self, $result ) = @_;

    # check for defaults
    if ( my @values = $self->get_default_value ) {
        return $self->_result_from_object( $result, \@values );
    }
    $self->init_state;
    $self->_set_result($result);
    my $count = $self->num_when_empty;
    my $index = 0;
    # build empty instance
    $self->fields( [] );
    while ( $count > 0 ) {
        my $field = $self->clone_element($index);
        my $result =
            HTML::FormHandler::Field::Result->new( name => $index, parent => $self->result );
        $result = $field->_result_from_fields($result);
        $self->result->add_result($result) if $result;
        $self->add_field($field);
        $index++;
        $count--;
    }
    $self->index($index);
    $self->_setup_for_js if $self->setup_for_js;
    $self->result->_set_field_def($self);
    return $result;
}

__PACKAGE__->meta->make_immutable;
use namespace::autoclean;
1;

__END__

=pod

=encoding UTF-8

=head1 NAME

HTML::FormHandler::Field::Repeatable - repeatable (array) field

=head1 VERSION

version 0.40057

=head1 SYNOPSIS

In a form, for an array of hashrefs, equivalent to a 'has_many' database
relationship.

  has_field 'addresses' => ( type => 'Repeatable' );
  has_field 'addresses.address_id' => ( type => 'PrimaryKey' );
  has_field 'addresses.street';
  has_field 'addresses.city';
  has_field 'addresses.state';

In a form, for an array of single fields (not directly equivalent to a
database relationship) use the 'contains' pseudo field name:

  has_field 'tags' => ( type => 'Repeatable' );
  has_field 'tags.contains' => ( type => 'Text',
       apply => [ { check => ['perl', 'programming', 'linux', 'internet'],
                    message => 'Not a valid tag' } ]
  );

or use 'contains' with single fields which are compound fields:

  has_field 'addresses' => ( type => 'Repeatable' );
  has_field 'addresses.contains' => ( type => '+MyAddress' );

If the MyAddress field contains fields 'address_id', 'street', 'city', and
'state', then this syntax is functionally equivalent to the first method
where the fields are declared with dots ('addresses.city');

You can pass attributes to the 'contains' field by supplying an 'init_contains' hashref.

    has_field 'addresses' => ( type => 'Repeatable,
       init_contains => { wrapper_attr => { class => ['hfh', 'repinst'] } },
    );

=head1 DESCRIPTION

This class represents an array. It can either be an array of hashrefs
(compound fields) or an array of single fields.

The 'contains' keyword is used for elements that do not have names
because they are not hash elements.

This field node will build arrays of fields from the parameters or an
initial object, or empty fields for an empty form.

The name of the element fields will be an array index,
starting with 0. Therefore the first array element can be accessed with:

   $form->field('tags')->field('0')
   $form->field('addresses')->field('0')->field('city')

or using the shortcut form:

   $form->field('tags.0')
   $form->field('addresses.0.city')

The array of elements will be in C<< $form->field('addresses')->fields >>.
The subfields of the elements will be in a fields array in each element.

   foreach my $element ( $form->field('addresses')->fields )
   {
      foreach my $field ( $element->fields )
      {
         # do something
      }
   }

Every field that has a 'fields' array will also have an 'error_fields' array
containing references to the fields that contain errors.

=head2 Complications

When new elements are created by a Repeatable field in a database form
an attempt is made to re-load the Repeatable field from the database, because
otherwise the repeatable elements will not have primary keys. Although this
works, if you have included other fields in your repeatable elements
that do *not* come from the database, the defaults/values must be
able to be loaded in a way that works when the form is initialized from
the database item. This is only an issue if you re-present the form
after the database update succeeds.

=head1 ATTRIBUTES

=over

=item index

This attribute contains the next index number available to create an
additional array element.

=item num_when_empty

This attribute (default 1) indicates how many empty fields to present
in an empty form which hasn't been filled from parameters or database
rows.

=item num_extra

When the field results are built from an existing object (item or init_object)
an additional number of repeatable elements will be created equal to this
number. Default is 0.

=item add_extra

When a form is submitted and the field results are built from the input
parameters, it's not clear when or if an additional repeatable element might
be wanted. The method 'add_extra' will add an empty repeatable element.

    $form->process( params => {....} );
    $form->field('my_repeatable')->add_extra(1);

This might be useful if the form is being re-presented to the user.

=item setup_for_js

    setup_for_js => 1

Saves information in the form for javascript to use when adding repeatable elements.
If using the example javascript, you also must set 'do_wrapper' in the
Repeatable field and use the Bootstrap widget wrapper (or wrap the repeatable
elements in a 'controls' div by setting tags => { controls_div => 1 }.
See t/repeatable/js.t for an example. See also
L<HTML::FormHandler::Render::RepeatableJs> and L<HTML::FormHandler::Field::AddElement>.

=back

=head1 AUTHOR

FormHandler Contributors - see HTML::FormHandler

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Gerda Shank.

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