1package Rose::HTML::Form::Repeatable;
2
3use strict;
4
5use Rose::HTML::Form;
6
7use base 'Rose::HTML::Object::Repeatable';
8
9our $VERSION = '0.616';
10
11__PACKAGE__->default_form_class('Rose::HTML::Form');
12
13#
14# Class methods
15#
16
17sub default_form_class { shift->default_prototype_class(@_) }
18
19#
20# Object methods
21#
22
23sub prototype_form       { shift->prototype(@_) }
24sub prototype_form_spec  { shift->prototype_spec(@_) }
25sub prototype_form_class { shift->prototype_class(@_) }
26sub prototype_form_clone { shift->prototype_clone(@_) }
27
28sub form_class { shift->prototype_form_class(@_) }
29
30sub is_repeatable_form { 1 }
31
32sub prepare
33{
34  my($self) = shift;
35  my(%args) = @_;
36
37  my $fq_form_name = quotemeta $self->fq_form_name;
38
39  my $re = qr(^$fq_form_name\.(\d+)\.);
40
41  my %have_num;
42
43  foreach my $param (keys %{$self->params})
44  {
45    if($param =~ $re)
46    {
47      my $num = $1;
48      $have_num{$num}++;
49      my $form = $self->form($num) || $self->make_form($num);
50    }
51  }
52
53  unless(%have_num)
54  {
55    if($self->default_count)
56    {
57      foreach my $num (1 .. $self->default_count)
58      {
59        $self->form($num) || $self->make_form($num);
60        $have_num{$num}++;
61      }
62    }
63    else
64    {
65      $self->delete_forms;
66    }
67  }
68
69  if(%have_num)
70  {
71    foreach my $form ($self->forms)
72    {
73      unless($have_num{$form->form_name})
74      {
75        $self->delete_form($form->form_name);
76      }
77    }
78  }
79
80  $self->SUPER::prepare(@_)  unless(delete $args{'init_only'});
81}
82
83sub init_fields
84{
85  my($self) = shift;
86  $self->prepare(init_only => 1);
87  $self->SUPER::init_fields(@_);
88}
89
90sub make_form
91{
92  my($self, $num) = @_;
93
94  Carp::croak "Missing form nubmer argument"  unless(@_ > 1);
95  Carp::croak "Form number argument must be greater than 0"  unless($num > 0);
96
97  my $form = $self->prototype_form_clone;
98
99  $form->rank($num);
100
101  $self->form_rank_counter($num + 1)  if($num >= $self->form_rank_counter);
102
103  $form->prepare;
104
105  $self->add_form($num => $form);
106
107  return $form;
108}
109
110sub make_next_form
111{
112  my ($self) = shift;
113  $self->increment_form_rank_counter; # XXX: Remove when form_rank_counter is removed
114  return $self->make_form($self->next_form_rank);
115}
116
117sub objects_from_form
118{
119  my($self) = shift;
120
121  my $method = 'object_from_form';
122
123  if(@_ > 1)
124  {
125    my %args = @_;
126    $method = $args{'method'}  if($args{'method'});
127  }
128
129  my @objects = map { $_->$method(@_) } $self->forms;
130
131  return wantarray ? @objects : \@objects;
132}
133
134sub init_with_objects
135{
136  my($self) = shift;
137
138  my $method = 'init_with_object';
139
140  my $objects;
141
142  if(@_ > 1)
143  {
144    my %args = @_;
145    $method = $args{'method'}  if($args{'method'});
146    $objects = $args{'objects'};
147  }
148
149  unless($objects)
150  {
151    $objects = \@_;
152  }
153
154  foreach my $form ($self->forms)
155  {
156    $form->$method(shift(@$objects));
157  }
158}
159
1601;
161
162__END__
163
164=head1 NAME
165
166Rose::HTML::Form::Repeatable - Repeatable sub-form automation.
167
168=head1 SYNOPSIS
169
170  package Person;
171
172  use base 'Rose::Object';
173
174  use Rose::Object::MakeMethods::Generic
175  (
176    scalar => [ 'name', 'age' ],
177    array  => 'emails',
178  );
179
180  ...
181
182  package Email;
183
184  use base 'Rose::Object';
185
186  use Rose::Object::MakeMethods::Generic
187  (
188    scalar =>
189    [
190      'address',
191      'type' => { check_in => [ 'home', 'work' ] },
192    ],
193  );
194
195  ...
196
197  package EmailForm;
198
199  use base 'Rose::HTML::Form';
200
201  sub build_form
202  {
203    my($self) = shift;
204
205    $self->add_fields
206    (
207      address     => { type => 'email', size => 50, required => 1 },
208      type        => { type => 'pop-up menu', choices => [ 'home', 'work' ],
209                       required => 1, default => 'home' },
210      save_button => { type => 'submit', value => 'Save Email' },
211    );
212  }
213
214  sub email_from_form { shift->object_from_form('Email') }
215  sub init_with_email { shift->init_with_object(@_) }
216
217  ...
218
219  package PersonEmailsForm;
220
221  use base 'Rose::HTML::Form';
222
223  sub build_form
224  {
225    my($self) = shift;
226
227    $self->add_fields
228    (
229      name        => { type => 'text',  size => 25, required => 1 },
230      age         => { type => 'integer', min => 0 },
231      save_button => { type => 'submit', value => 'Save Person' },
232    );
233
234    ##
235    ## The important part happens here: add a repeatable form
236    ##
237
238    # A person can have zero or more emails
239    $self->add_repeatable_form(emails => EmailForm->new);
240
241    # Alternate ways to add the same repeatable form:
242    #
243    # Name/hashref pair:
244    # $self->add_repeatable_form(emails => { form_class => 'EmailForm' });
245    #
246    # Using the generic add_form() method:
247    # $self->add_form
248    # (
249    #   emails =>
250    #   {
251    #     form_class    => 'EmailForm',
252    #     default_count => 0,
253    #     repeatable    => 1,
254    #   }
255    # );
256    #
257    # See the documentation for Rose::HTML::Form's add_forms() and
258    # add_repeatable_forms() methods for more information.
259  }
260
261  sub init_with_person
262  {
263    my($self, $person) = @_;
264
265    $self->init_with_object($person);
266
267    # Delete any existing email forms and create
268    # the appropriate number for this $person
269
270    my $email_form = $self->form('emails');
271    $email_form->delete_forms;
272
273    my $i = 1;
274
275    foreach my $email ($person->emails)
276    {
277      $email_form->make_form($i++)->init_with_email($email);
278    }
279  }
280
281  sub person_from_form
282  {
283    my($self) = shift;
284
285    my $person = $self->object_from_form(class => 'Person');
286
287    my @emails;
288
289    foreach my $form ($self->form('emails')->forms)
290    {
291      push(@emails, $form->email_from_form);
292    }
293
294    $person->emails(@emails);
295
296    return $person;
297  }
298
299=head1 DESCRIPTION
300
301L<Rose::HTML::Form::Repeatable> provides a convenient way to include zero or more copies of a nested form.  See the L<nested forms|Rose::HTML::Form/"NESTED FORMS"> section of the L<Rose::HTML::Form> documentation for some essential background information.
302
303L<Rose::HTML::Form::Repeatable> works like a wrapper for an additional level of sub-forms.  The L<Rose::HTML::Form::Repeatable> object itself has no fields.  Instead, it has a list of zero or more sub-forms, each of which is named with a positive integer greater than zero.
304
305The L<synopsis|/SYNOPSIS> above contains a full example.  In it, the C<PersonEmailsForm> contains zero or more L<EmailForm> sub-forms under the name C<emails>.  The C<emails> name identifies the L<Rose::HTML::Form::Repeatable> object, while C<emails.N> identifies each L<EmailForm> object contained within it (e.g., C<emails.1>, C<emails.2>, etc.).
306
307Each repeated form must be of the same class.  A repeated form can be generated by cloning a L<prototype form|/prototype_form> or by instantiating a specified L<prototype form class|/prototype_form_class>.
308
309A repeatable form decides how many of each repeated sub-form it should contain based on the contents of the query parameters (contained in the L<params|Rose::HTML::Form/params> attribute for the parent form).  If there are no L<params|Rose::HTML::Form/params>, then the L<default_count|/default_count> determines the number of repeated forms.
310
311Repeated forms are created in response to the L<init_fields|/init_fields> or L<prepare|/prepare> methods being called.  In the L<synopsis|/SYNOPSIS> example, the C<person_from_form> method does not need to create, delete, or otherwise set up the repeated email sub-forms because it can sensibly assume that the  L<init_fields|/init_fields> and/or L<prepare|/prepare> methods have been called already.  On the other hand, the C<init_with_person> method must configure the repeated email forms based on the number of email addresses contained in the C<Person> object that it was passed.
312
313On the client side, the usual way to handle repeated sub-forms is to make an AJAX request for new content to add to an existing form.  The L<make_form|/make_form> method is designed to do exactly that, returning a correctly namespaced L<Rose::HTML::Form>-derived object ready to have its fields serialized (usually through a template) into HTML which is then inserted into the existing form on a web page.
314
315This class inherits from and follows the conventions of L<Rose::HTML::Form>. Inherited methods that are not overridden will not be documented a second time here.  See the L<Rose::HTML::Form> documentation for more information.
316
317=head1 CONSTRUCTOR
318
319=over 4
320
321=item B<new PARAMS>
322
323Constructs a new L<Rose::HTML::Form::Repeatable> object based on PARAMS, where PARAMS are name/value pairs.  Any object method is a valid parameter name.
324
325=back
326
327=head1 CLASS METHODS
328
329=over 4
330
331=item B<default_form_class [CLASS]>
332
333Get or set the name of the default L<Rose::HTML::Form>-derived class of the repeated form.  The default value is L<Rose::HTML::Form>.
334
335=back
336
337=head1 OBJECT METHODS
338
339=over 4
340
341=item B<default_count [INT]>
342
343Get or set the default number of repeated forms to create in the absence of any L<parameters|Rose::HTML::Form/params>.  The default value is zero.
344
345=item B<empty_is_ok [BOOL]>
346
347Get or set a boolean value that indicates whether or not it's OK for a repeated form to be empty.  (That is, validation should not fail if the entire sub-form is empty, even if the sub-form has required fields.)  Defaults to false.
348
349=item B<init_fields>
350
351In addition to doing all the usual things that the L<base class implementation|Rose::HTML::Form/init_fields> does, this method creates or deletes repeated sub-forms as necessary to make sure they match the query L<parameters|Rose::HTML::Form/params>, if present, or the L<default_count|/default_count> if there are no L<parameters|Rose::HTML::Form/params> that apply to any of the sub-forms.
352
353=item B<init_with_objects [ OBJECTS | PARAMS ]>
354
355Given a list of OBJECTS or name/value pairs PARAMS, initialize each sub-form, taking one object from the list and passing it to a method called on each sub-form.  The first object is passed to the first form, the second object to the second form, and so on.  (Form order is determined by the the order forms are returned from the L<forms|Rose::HTML::Form/forms> method.)
356
357Valid parameters are:
358
359=over 4
360
361=item B<objects ARRAYREF>
362
363A reference to an array of objects with which to initialize the form(s).  This parameter is required if PARAMS are passed.
364
365=item B<method NAME>
366
367The name of the method to call on each sub-form.  The default value is C<init_with_object>.
368
369=back
370
371=item B<make_form INT>
372
373Given an integer argument greater than zero, create, add to the form, and return a new numbered L<prototype form clone|/prototype_form_clone> object.
374
375=item B<make_next_form>
376
377Create, add to the form, and return a new numbered L<prototype form clone|/prototype_form_clone> object whose L<rank|Rose::HTML::Form/rank> is one greater than the the highest-ranking existing sub-form.
378
379=item B<objects_from_form [PARAMS]>
380
381Return a list (in list context) or reference to an array (in scalar context) of objects corresponding to the list of repeated sub-forms.  This is done by calling a method on each sub-form and collecting the return values.  Name/value parameters may be passed.  Valid parameters are:
382
383=over 4
384
385=item B<method NAME>
386
387The name of the method to call on each sub-form.  The default value is C<object_from_form>.
388
389=back
390
391=item B<prepare>
392
393This method does the same thing as the L<init_fields|/init_fields> method, but calls through to the L<base class prepare|Rose::HTML::Form/prepare> method rather than the  L<base class init_fields|Rose::HTML::Form/init_fields> method.
394
395=item B<prototype_form [FORM]>
396
397Get or set the L<Rose::HTML::Form>-derived object used as the prototype for each repeated form.
398
399=item B<prototype_form_class [CLASS]>
400
401Get or set the name of the L<Rose::HTML::Form>-derived class used by the L<prototype_form_clone|/prototype_form_clone> method to create each repeated sub-form.  The default value is determined by the L<default_form_class|/default_form_class> class method.
402
403=item B<prototype_form_spec [SPEC]>
404
405Get or set the specification for the L<Rose::HTML::Form>-derived object used as the prototype for each repeated form.  The SPEC can be a reference to an array, a reference to a hash, or a list that will be coerced into a reference to an array.  In the absence of a L<prototype_form|/prototype_form>, the SPEC is dereferenced and passed to the C<new()> method called on the L<prototype_form_class|/prototype_form_class> in order to create each L<prototype_form_clone|/prototype_form_clone>.
406
407=item B<prototype_form_clone>
408
409Returns a clone of the L<prototype_form|/prototype_form>, if one was set.  Otherwise, creates and returns a new L<prototype_form_class|/prototype_form_class> object, passing the L<prototype_form_spec|/prototype_form_spec> to the constructor.
410
411=back
412
413=head1 AUTHOR
414
415John C. Siracusa (siracusa@gmail.com)
416
417=head1 LICENSE
418
419Copyright (c) 2010 by John C. Siracusa.  All rights reserved.  This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
420