File Coverage

blib/lib/Rose/HTML/Form/Repeatable.pm
Criterion Covered Total %
statement 52 72 72.2
branch 18 32 56.2
condition 2 3 66.6
subroutine 13 16 81.2
pod 12 13 92.3
total 97 136 71.3


line stmt bran cond sub pod time code
1             package Rose::HTML::Form::Repeatable;
2              
3 2     2   15 use strict;
  2         3  
  2         84  
4              
5 2     2   13 use Rose::HTML::Form;
  2         4  
  2         26  
6              
7 2     2   11 use base 'Rose::HTML::Object::Repeatable';
  2         3  
  2         1198  
8              
9             our $VERSION = '0.616';
10              
11             __PACKAGE__->default_form_class('Rose::HTML::Form');
12              
13             #
14             # Class methods
15             #
16              
17 2     2 1 16 sub default_form_class { shift->default_prototype_class(@_) }
18              
19             #
20             # Object methods
21             #
22              
23 9     9 1 135 sub prototype_form { shift->prototype(@_) }
24 3     3 1 38 sub prototype_form_spec { shift->prototype_spec(@_) }
25 4     4 1 65 sub prototype_form_class { shift->prototype_class(@_) }
26 32     32 1 96 sub prototype_form_clone { shift->prototype_clone(@_) }
27              
28 0     0 1 0 sub form_class { shift->prototype_form_class(@_) }
29              
30 4     4 0 18 sub is_repeatable_form { 1 }
31              
32             sub prepare
33             {
34 33     33 1 53 my($self) = shift;
35 33         81 my(%args) = @_;
36              
37 33         89 my $fq_form_name = quotemeta $self->fq_form_name;
38              
39 33         416 my $re = qr(^$fq_form_name\.(\d+)\.);
40              
41 33         72 my %have_num;
42              
43 33         57 foreach my $param (keys %{$self->params})
  33         98  
44             {
45 134 100       684 if($param =~ $re)
46             {
47 75         179 my $num = $1;
48 75         153 $have_num{$num}++;
49 75   66     182 my $form = $self->form($num) || $self->make_form($num);
50             }
51             }
52              
53 33 100       106 unless(%have_num)
54             {
55 9 100       29 if($self->default_count)
56             {
57 8         62 foreach my $num (1 .. $self->default_count)
58             {
59 14 100       77 $self->form($num) || $self->make_form($num);
60 14         43 $have_num{$num}++;
61             }
62             }
63             else
64             {
65 1         39 $self->delete_forms;
66             }
67             }
68              
69 33 100       86 if(%have_num)
70             {
71 32         104 foreach my $form ($self->forms)
72             {
73 73 100       206 unless($have_num{$form->form_name})
74             {
75 11         43 $self->delete_form($form->form_name);
76             }
77             }
78             }
79              
80 33 100       211 $self->SUPER::prepare(@_) unless(delete $args{'init_only'});
81             }
82              
83             sub init_fields
84             {
85 28     28 1 57 my($self) = shift;
86 28         79 $self->prepare(init_only => 1);
87 28         128 $self->SUPER::init_fields(@_);
88             }
89              
90             sub make_form
91             {
92 31     31 1 90 my($self, $num) = @_;
93              
94 31 50       66 Carp::croak "Missing form nubmer argument" unless(@_ > 1);
95 31 50       73 Carp::croak "Form number argument must be greater than 0" unless($num > 0);
96              
97 31         66 my $form = $self->prototype_form_clone;
98              
99 31         638 $form->rank($num);
100              
101 31 100       131 $self->form_rank_counter($num + 1) if($num >= $self->form_rank_counter);
102              
103 31         296 $form->prepare;
104              
105 31         307 $self->add_form($num => $form);
106              
107 31         158 return $form;
108             }
109              
110             sub make_next_form
111             {
112 3     3 1 9 my ($self) = shift;
113 3         16 $self->increment_form_rank_counter; # XXX: Remove when form_rank_counter is removed
114 3         10 return $self->make_form($self->next_form_rank);
115             }
116              
117             sub objects_from_form
118             {
119 0     0 1   my($self) = shift;
120              
121 0           my $method = 'object_from_form';
122              
123 0 0         if(@_ > 1)
124             {
125 0           my %args = @_;
126 0 0         $method = $args{'method'} if($args{'method'});
127             }
128              
129 0           my @objects = map { $_->$method(@_) } $self->forms;
  0            
130              
131 0 0         return wantarray ? @objects : \@objects;
132             }
133              
134             sub init_with_objects
135             {
136 0     0 1   my($self) = shift;
137              
138 0           my $method = 'init_with_object';
139              
140 0           my $objects;
141              
142 0 0         if(@_ > 1)
143             {
144 0           my %args = @_;
145 0 0         $method = $args{'method'} if($args{'method'});
146 0           $objects = $args{'objects'};
147             }
148              
149 0 0         unless($objects)
150             {
151 0           $objects = \@_;
152             }
153              
154 0           foreach my $form ($self->forms)
155             {
156 0           $form->$method(shift(@$objects));
157             }
158             }
159              
160             1;
161              
162             __END__
163              
164             =head1 NAME
165              
166             Rose::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              
301             L<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              
303             L<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              
305             The 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              
307             Each 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              
309             A 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              
311             Repeated 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              
313             On 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              
315             This 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              
323             Constructs 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              
333             Get 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              
343             Get 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              
347             Get 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              
351             In 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              
355             Given 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              
357             Valid parameters are:
358              
359             =over 4
360              
361             =item B<objects ARRAYREF>
362              
363             A 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              
367             The 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              
373             Given 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              
377             Create, 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              
381             Return 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              
387             The 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              
393             This 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              
397             Get 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              
401             Get 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              
405             Get 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              
409             Returns 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              
415             John C. Siracusa (siracusa@gmail.com)
416              
417             =head1 LICENSE
418              
419             Copyright (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.