PageRenderTime 112ms CodeModel.GetById 27ms app.highlight 76ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/HTML/FormHandler/Manual/Cookbook.pod

http://github.com/gshank/html-formhandler
Unknown | 799 lines | 606 code | 193 blank | 0 comment | 0 complexity | 6fb25a8f409e5609811de4a2ff9fc697 MD5 | raw file
  1package HTML::FormHandler::Manual::Cookbook;
  2# ABSTRACT: FormHandler use recipes
  3
  4=head1 SYNOPSIS
  5
  6L<Manual Index|HTML::FormHandler::Manual>
  7
  8Collection of use recipes for L<HTML::FormHandler>
  9
 10=head2 No form file, no template file...
 11
 12I had to create a tiny little form this week for admins to enter a comment, and
 13it seemed silly to have to create a form file and a template file. I remembered
 14that you can set the TT 'template' to a string reference and not use a template
 15at all, which is nice when FormHandler will create the form HTML for you anyway.
 16
 17    sub comment : Chained('base_sub') PathPart('comment') Args(0) {
 18        my ( $self, $c ) = @_;
 19
 20        my $form = HTML::FormHandler->new( field_list =>
 21            [ comment => { type => 'Text', size => 60 },
 22              submit => {type => 'Submit'} ] );
 23        $form->process($c->req->params);
 24        if ( $form->validated ) {
 25            $self->admin_log( $c, "Admin::Queue", "admin comment",
 26                  $form->field('comment')->value );
 27            $c->flash( message => 'Comment added' );
 28            $c->res->redirect( $c->stash->{urilist}->{view} );
 29        }
 30        my $rendered_form = $form->render;
 31        $c->stash( template => \$rendered_form );
 32    }
 33
 34This creates the form on the fly with a comment field and a submit button,
 35renders it using the default TT wrappers, then logs the comment. No other files
 36at all....
 37
 38FormHandler isn't really necessary for validation here, but it does make it
 39possible to have a simple, standalone method.
 40
 41
 42=head2 Dynamically change the active fields
 43
 44A common use case is for forms with some fields that should be displayed in
 45some circumstances and not in others. There are a number of ways to do this.
 46One way is to use the 'field_list' method:
 47
 48   sub field_list {
 49      my $self = shift;
 50      my @fields;
 51      <build list of fields>
 52      return \@fields;
 53   }
 54
 55This only happens at form construction time, however. Another method that
 56works is to define all of the possible fields in your form, and mark some
 57of them 'inactive';
 58
 59   package MyApp::Variable::Form;
 60   use HTML::FormHandler::Moose;
 61   extends 'HTML::FormHandler';
 62
 63   has_field 'foo';
 64   has_field 'bar' => ( inactive => 1 );
 65   1;
 66
 67Set to 'active' or 'inactive' on the 'process' call:
 68
 69   $form->process( params => $params, active => ['foo', 'bar'] );
 70   ...
 71   $form->process( params => $params, inactive => ['bar'] );
 72
 73If you need to check some other state to determine whether or not a field should
 74be active, you can do that using a Moose method modifier on 'set_active':
 75
 76   before 'set_active' => sub {
 77      my $self = shift;
 78      $self->active(['foo', bar']) if ( <some_condition> );
 79   };
 80
 81Fields set to active/inactive on the 'process' call are automatically set back
 82to inactive when the form is cleared, so there's no need to reset.
 83
 84If you want the fields activated for the life of an object, set active on new:
 85
 86    my $form = MyApp::Form::User->new( active => ['opt_in', 'active']);
 87
 88=head2 Add custom attributes to FormHandler fields
 89
 90If you want to add custom attributes to the FormHandler fields but don't want
 91to subclass all the fields, you can apply a role containing the new
 92attributes to an L<HTML::FormHandler::Field> in your form.
 93
 94Use 'traits' on the individual fields to apply a role to field instances.
 95Use the form attribute 'field_traits' to apply a role to all field instances in
 96the form.
 97
 98    package MyApp::Form::Test;
 99    use HTML::FormHandler::Moose;
100    extends 'HTML::FormHandler';
101
102    has_field 'foo' => ( traits => ['MyApp::TraitFor::Test'] );
103    has '+field_traits' => ( default => sub { ['Some::Trait', 'Another::Trait'] } );
104
105Or set the traits on new:
106
107    my $form = MyApp::Form::User->new( field_traits => ['MyApp::TraitFor::Test'] );
108    my $form = MyApp::Form::User->new(
109             field_list => [ '+foo' => { traits => [...] } ]);
110
111To apply the role to a field base class, use 'apply_traits' on that class:
112
113    HTML::FormHandler::Field->apply_traits( 'Some::Test' );
114    HTML::FormHandler::Field::Text->apply_traits( 'Another::Trait' );
115
116=head2 Select lists
117
118If you want to set the default value of a select field to 0, you can just
119use 'default' on the field:
120
121   has_field 'license' => ( default => 0 );
122
123If there is logic involved, you can use a 'default_<field_name>' method:
124
125   sub default_license {
126      my ( $self, $field, $item ) = @_;
127      return 0 unless $item && $item->license_id;
128      return $item->license_id;
129   }
130
131If the table defining the choices for a select list doesn't include
132a 'no choice' choice, you can set 'empty_select' in your field if you
133are using FormHandler rendering:
134
135   has_field 'subject_class' => ( type => 'Select',
136      empty_select => '--- Choose Subject Class ---' );
137
138Or you can do in a template:
139
140   [% f = form.field('subject_class') %]
141   <select id="select_sc" name="[% f.name %]">
142     <option value="">--- Choose Subject Class---</option>
143     [% FOR option IN f.options %]
144       <option value="[% option.value %]"
145          [% IF option.value == f.fif %]selected="selected"[% END %]>
146          [% option.label | html %]</option>
147     [% END %]
148   </select>
149
150You can create a custom select list in an 'options_' method:
151
152   sub options_country {
153      my $self = shift;
154      return unless $self->schema;
155      my @rows =
156         $self->schema->resultset( 'Country' )->
157            search( {}, { order_by => ['rank', 'country_name'] } )->all;
158      return [ map { $_->digraph, $_->country_name } @rows ];
159   }
160
161=head2 The database and FormHandler forms
162
163If you have to process the input data before saving to the database, and
164this is something that would be useful in other places besides your form,
165you should do that processing in the DBIx::Class result class.
166
167If the pre-processing is only relevant to HTML form input, you might want
168to do it in the form by setting a flag to prevent database updates, performing
169the pre-processing, and then updating the database yourself.
170
171   has_field 'my_complex_field' => ( type => 'Text', noupdate => 1 );
172
173The 'noupdate' flag is set in order to skip an attempt to update the database
174for this field (it would not be necessary if the field doesn't actually exist
175in the database...).  You can process the input for the non-updatable field
176field in a number of different places, depending on what is most logical.
177Some of the choices are:
178
179   1) validate (for the form or field)
180   2) validate_model
181   3) update_model
182
183When the field is flagged 'writeonly', the value from the database will not
184be used to fill in the form (put in the C<< $form->fif >> hash, or the
185field C<< $field->fif >>), but a value entered in the form WILL be used
186to update the database.
187
188If you want to enter fields from an additional table that is related to
189this one in a 'single' relationship, you can use the DBIx::Class 'proxy'
190feature to create accessors for those fields.
191
192=head2 Set up form base classes or roles for your application
193
194You can add whatever attributes you want to your form classes. Maybe you
195want to save a title, or a particular navigation widget. You could even
196save bits of text, or retrieve them from the database.
197
198   package MyApp::Form::Base;
199   use Moose;
200   extends 'HTML::FormHandler::Model::DBIC';
201
202   has 'title' => ( isa => 'Str', is => 'rw' );
203   has 'nav_bar' => ( isa => 'Str', is => 'rw' );
204   has_block 'reg_header' => ( tag => 'fieldset', label => 'Registration form',
205       content => 'We take your membership seriously...' );
206
207   sub summary {
208      my $self = shift;
209      my $schema = $self->schema;
210      my $text = $schema->resultset('Summary')->find( ... )->text;
211      return $text;
212   }
213   1;
214
215Then:
216
217   package MyApp::Form::Whatsup;
218   use Moose;
219   extends 'MyApp::Form::Base';
220
221   has '+title' => ( default => 'This page is an example of what to expect...' );
222   has '+nav_bar' => ( default => ... );
223   ...
224   1;
225
226And in the template:
227
228   <h1>[% form.title %]</h1>
229   [% form.nav_bar %]
230   [% form.block('reg_header')->render %]
231   <p><b>Summary: </b>[% form.summary %]</p>
232
233Or you can make these customizations Moose roles.
234
235   package MyApp::Form::Role::Base;
236   use Moose::Role;
237   ...
238
239   package MyApp::Form::Whatsup;
240   use Moose;
241   with 'MyApp::Form::Role::Base';
242   ...
243
244=head2 Split up your forms into reusable pieces
245
246An address field:
247
248   package Form::Field::Address;
249   use HTML::FormHandler::Moose;
250   extends 'HTML::FormHandler::Field::Compound';
251
252   has_field 'street';
253   has_field 'city';
254   has_field 'state' => ( type => 'Select', options_method => \&options_state );
255   has_field 'zip' => ( type => '+Zip' );
256
257   sub options_state {
258     ...
259   }
260
261   no HTML::FormHandler::Moose;
262   1;
263
264A person form that includes an address field:
265
266   package Form::Person;
267   use HTML::FormHandler::Moose;
268   extends 'HTML::FormHandler';
269
270   has '+widget_name_space' => ( default => sub {['Form::Field']} );
271   has_field 'name';
272   has_field 'telephone';
273   has_field 'email' => ( type => 'Email' );
274   has_field 'address' => ( type => 'Address' );
275
276   sub validate_name {
277    ....
278   }
279
280   no HTML::FormHandler::Moose;
281   1;
282
283Or you can use roles;
284
285   package Form::Role::Address;
286   use HTML::FormHandler::Moose::Role;
287
288   has_field 'street';
289   has_field 'city';
290   has_field 'state' => ( type => 'Select' );
291   has_field 'zip' => ( type => '+Zip' );
292
293   sub options_state {
294     ...
295   }
296
297   no HTML::FormHandler::Moose::Role;
298   1;
299
300You could make roles that are collections of validations:
301
302   package Form::Role::Member;
303   use Moose::Role;
304
305   sub check_zip {
306      ...
307   }
308   sub check_email {
309      ...
310   }
311
312   1;
313
314And if the validations apply to fields with different names, specify the
315'validate_method' on the fields:
316
317   with 'Form::Role::Member';
318   has_field 'zip' => ( type => 'Integer', validate_method => \&check_zip );
319
320
321=head2 Access a user record in the form
322
323You might need the user_id to create specialized select lists, or do other form processing. Add a user_id attribute to your form:
324
325  has 'user_id' => ( isa => 'Int', is => 'rw' );
326
327Then pass it in when you process the form:
328
329  $form->process( item => $item, params => $c->req->parameters, user_id => $c->user->user_id );
330
331=head2 Handle extra database fields
332
333If there is another database field that needs to be updated when a row is
334created, add an attribute to the form, and then process it with
335C< before 'update_model' >.
336
337In the form:
338
339    has 'hostname' => ( isa => 'Int', is => 'rw' );
340
341    before 'update_model' => sub {
342       my $self = shift;
343       $self->item->hostname( $self->hostname );
344    };
345
346Then just use an additional parameter when you create/process your form:
347
348    $form->process( item => $item, params => $params, hostname => $c->req->host );
349
350Some kinds of DB relationships need to have primary keys which might be more easily
351set in the update_model method;
352
353    sub update_model {
354        my $self = shift;
355        my $values = $self->values;
356        $values->{some_field}->{some_key} = 'some_value';
357        $self->_set_value($values);
358        $self->next::method;
359    }
360
361If you need to access a database field in order to create the value for a
362form field you can use a C< default_* > method.
363
364    sub default_myformfield {
365        my ($self, $field, $item) = @_;
366        return unless defined $item;
367        my $databasefield =  $item->databasefield;
368        my $value = ... # do stuff
369        return $value;
370    }
371
372=head2 Additional changes to the database
373
374If you want to do additional database updates besides the ones that FormHandler
375does for you, the best solution would generally be to add the functionality to
376your result source or resultset classes, but if you want to do additional updates
377in a form you should use an 'around' method modifier and a transaction:
378
379  around 'update_model' => sub {
380      my $orig = shift;
381      my $self = shift;
382      my $item = $self->item;
383
384      $self->schema->txn_do( sub {
385          $self->$orig(@_);
386
387          <perform additional updates>
388      });
389  };
390
391=head2 Doing cross validation in roles
392
393In a role that handles a number of different fields, you may want to
394perform cross validation after the individual fields are validated.
395In the form you could use the 'validate' method, but that doesn't help
396if you want to keep the functionality packaged in a role. Instead you
397can use the 'after' method modifier on the 'validate' method:
398
399
400   package MyApp::Form::Roles::DateFromTo;
401
402   use HTML::FormHandler::Moose::Role;
403   has_field 'date_from' => ( type => 'Date' );
404   has_field 'date_to'   => ( type => 'Date' );
405
406   after 'validate' => sub {
407      my $self = shift;
408      $self->field('date_from')->add_error('From date must be before To date')
409         if $self->field('date_from')->value gt $self->field('date_to')->value;
410   };
411
412=head2 Changing required flag
413
414Sometimes a field is required in one situation and not required in another.
415You can use a method modifier before 'validate_form':
416
417   before 'validate_form' => sub {
418      my $self = shift;
419      my $required = 0;
420      $required = 1
421         if( $self->params->{field_name} eq 'something' );
422      $self->field('some_field')->required($required);
423   };
424
425This happens before the fields contain input or values, so you would need to
426look at the param value. If you need the validated value, it might be better
427to do these sort of checks in the form's 'validate' routine.
428
429   sub validate {
430      my $self = shift;
431      $self->field('dependent_field')->add_error("Field is required")
432          if( $self->field('some_field')->value eq 'something' &&
433              !$self->field('dependent_field')->has_value);
434   }
435
436In a Moose role you would need to use a method modifier instead.
437
438   after 'validate' => sub { ... };
439
440Don't forget the dependency list, which is used for cases where if any of one
441of a group of fields has a value, all of the fields are required.
442
443=head2 Supply an external coderef for validation
444
445There are situations in which you need to use a subroutine for validation
446which is not logically part of the form. It's possible to pass in a context
447or other sort of pointer and call the routine in the form's validation
448routine, but that makes the architecture muddy and is not a clear separation
449of concerns.
450
451This is an example of how to supply a coderef when constructing the form that
452performs validation and can be used to set an appropriate error
453using L<Moose::Meta::Attribute::Native::Trait::Code>.
454(Thanks to Florian Ragwitz for this excellent idea...)
455
456Here's the form:
457
458    package SignupForm;
459    use HTML::FormHandler::Moose;
460    extends 'HTML::FormHandler';
461
462    has check_name_availability => (
463        traits   => ['Code'],
464        isa      => 'CodeRef',
465        required => 1,
466        handles  => { name_available => 'execute', },
467    );
468
469    has_field 'name';
470    has_field 'email';
471
472    sub validate {
473        my $self = shift;
474        my $name = $self->value->{name};
475        if ( defined $name && length $name && !$self->name_available($name) ) {
476            $self->field('name')->add_error('That name is taken already');
477        }
478    }
479    1;
480
481And here's where the coderef is passed in to the form.
482
483    package MyApp::Signup;
484    use Moose;
485
486    has 'form' => ( is => 'ro', builder => 'build_form' );
487    sub build_form {
488        my $self = shift;
489        return SignupForm->new(
490            {
491                check_name_availability => sub {
492                    my $name = shift;
493                    return $self->username_available($name);
494                },
495            }
496        );
497
498    }
499    sub username_available {
500        my ( $self, $name ) = @_;
501        # perform some sort of username availability checks
502    }
503    1;
504
505=head2 Example of a form with custom database interface
506
507The default DBIC model requires that the form structure match the database
508structure. If that doesn't work - you need to present the form in a different
509way - you may need to fudge it by creating your own 'init_object' and doing
510the database updates in the 'update_model' method.
511
512Here is a working example for a 'family' object (equivalent to a 'user'
513record') that has a relationship to permission type roles in a relationship
514'user_roles'.
515
516    package My::Form::AdminRoles;
517    use HTML::FormHandler::Moose;
518    extends 'HTML::FormHandler';
519
520    has 'schema' => ( is => 'ro', required => 1 );  # Note 1
521    has '+widget_wrapper' => ( default => 'None' ); # Note 2
522
523    has_field 'admin_roles' => ( type => 'Repeatable' ); # Note 3
524    has_field 'admin_roles.family'    => ( type => 'Hidden' ); # Note 4
525    has_field 'admin_roles.family_id' => ( type => 'PrimaryKey' ); # Note 5
526    has_field 'admin_roles.admin_flag' => ( type => 'Boolean', label => 'Admin' );
527
528    # Note 6
529    sub init_object {
530        my $self = shift;
531
532        my @is_admin;
533        my @is_not_admin;
534        my $active_families = $self->schema->resultset('Family')->search( { active => 1 } );
535        while ( my $fam = $active_families->next ) {
536            my $admin_flag =
537                 $fam->search_related('user_roles', { role_id => 2 } )->count > 0 ? 1 : 0;
538            my $family_name = $fam->name1 . ", " . $fam->name2;
539            my $elem =  { family => $family_name, family_id => $fam->family_id,
540                 admin_flag => $admin_flag };
541            if( $admin_flag ) {
542                push @is_admin, $elem;
543            }
544            else {
545                push @is_not_admin, $elem;
546            }
547        }
548        # Note 7
549        # sort into admin flag first, then family_name
550        @is_admin = sort { $a->{family} cmp $b->{family} } @is_admin;
551        @is_not_admin = sort { $a->{family} cmp $b->{family} } @is_not_admin;
552        return { admin_roles => [@is_admin, @is_not_admin] };
553    }
554
555    # Note 8
556    sub update_model {
557        my $self = shift;
558
559        my $families = $self->schema->resultset('Family');
560        my $family_roles = $self->value->{admin_roles};
561        foreach my $elem ( @{$family_roles} ) {
562            my $fam = $families->find( $elem->{family_id} );
563            my $has_admin_flag = $fam->search_related('user_roles', { role_id => 2 } )->count > 0;
564            if( $elem->{admin_flag} == 1 && !$has_admin_flag ) {
565                $fam->create_related('user_roles', { role_id => 2 } );
566            }
567            elsif( $elem->{admin_flag} == 0 && $has_admin_flag ) {
568                $fam->delete_related('user_roles', { role_id => 2 } );
569            }
570        }
571    }
572
573Note 1: This form creates its own 'schema' attribute. You could inherit from
574L<HTML::FormHandler::Model::DBIC>, but you won't be using its update code, so
575it wouldn't add much.
576
577Note 2: The form will be displayed with a template that uses 'bare' form input
578fields, so 'widget_wrapper' is set to 'None' to skip wrapping the form inputs with
579divs or table elements.
580
581Note 3: This form consists of an array of elements, so there will be a single
582Repeatable form field with subfields. If you wanted to use automatic rendering, you would
583also need to create a 'submit' field, but in this case it will just be done
584in the template.
585
586Note 4: This field is actually going to be used for display purposes only, but it's
587a hidden field because otherwise the information would be lost when displaying
588the form from parameters. For this case there is no real 'validation' so it
589might not be necessary, but it would be required if the form needed to be
590re-displayed with error messages.
591
592Note 5: The 'family_id' is the primary key field, necessary for updating the
593correct records.
594
595Note 6: 'init_object' method: This is where the initial object is created, which
596takes the place of a database row for form creation.
597
598Note 7: The entries with the admin flag turned on are sorted into the beginning
599of the list. This is entirely a user interface choice.
600
601Note 8: 'update_model' method: This is where the database updates are performed.
602
603
604The Template Toolkit template for this form:
605
606    <h1>Update admin status for members</h1>
607    <form name="adminroles" method="POST" action="[% c.uri_for('admin_roles') %]">
608      <input class="submit" name="submit" value="Save" type="submit">
609    <table border="1">
610      <th>Family</th><th>Admin</th>
611      [% FOREACH f IN form.field('admin_roles').sorted_fields %]
612         <tr>
613         <td><b>[% f.field('family').fif %]</b>[% f.field('family').render %]
614         [% f.field('family_id').render %]</td><td> [% f.field('admin_flag').render %]</td>
615         </tr>
616      [% END %]
617    </table>
618      <input class="submit" name="submit" value="Save" type="submit">
619    </form
620
621The form is rendered in a simple table, with each field rendered using the
622automatically installed rendering widgets with no wrapper (widget_wrapper => 'None').
623There are two hidden fields here, so what is actually seen is two columns, one with
624the user (family) name, the other with a checkbox showing whether the user has
625admin status. Notice that the 'family' field information is rendered twice: once
626as a hidden field that will allow it to be preserved in params, once as a label.
627
628The Catalyst controller action to execute the form:
629
630    sub admin_roles : Local {
631        my ( $self, $c ) = @_;
632
633        my $schema = $c->model('DB')->schema;
634        my $form = My::Form::AdminRoles->new( schema => $schema );
635        $form->process( params => $c->req->params );
636        # re-process if form validated to reload from db and re-sort
637        $form->process( params => {}) if $form->validated;
638        $c->stash( form => $form, template => 'admin/admin_roles.tt' );
639        return;
640    }
641
642Rather than redirect to some other page after saving the form, the form is redisplayed.
643If the form has been validated (i.e. the 'update_model' method has been run), the
644'process' call is run again in order to re-sort the displayed list with admin users at
645the top. That could have also been done in the 'update_model' method.
646
647=head2 A form that takes a resultset, with custom update_model
648
649For updating a Repeatable field that is filled from a Resultset, and not a
650relationship on a single row. Creates a 'resultset' attribute to pass in
651a resultset. Massages the data into an array that's pointed to by an
652'employers' hash key, and does the reverse in the 'update_model' method.
653Yes, it's a kludge, but it could be worse. If you want to implement a more
654general solution, patches welcome.
655
656    package Test::Resultset;
657    use HTML::FormHandler::Moose;
658    extends 'HTML::FormHandler::Model::DBIC';
659
660    has '+item_class' => ( default => 'Employer' );
661    has 'resultset' => ( isa => 'DBIx::Class::ResultSet', is => 'rw',
662            trigger => sub { shift->set_resultset(@_) } );
663    sub set_resultset {
664        my ( $self, $resultset ) = @_;
665        $self->schema( $resultset->result_source->schema );
666    }
667    sub init_object {
668        my $self = shift;
669        my $rows = [$self->resultset->all];
670        return { employers => $rows };
671    }
672    has_field 'employers' => ( type => 'Repeatable' );
673    has_field 'employers.employer_id' => ( type => 'PrimaryKey' );
674    has_field 'employers.name';
675    has_field 'employers.category';
676    has_field 'employers.country';
677
678    sub update_model {
679        my $self = shift;
680        my $values = $self->values->{employers};
681        foreach my $row (@$values) {
682            delete $row->{employer_id} unless defined $row->{employer_id};
683            $self->resultset->update_or_create( $row );
684        }
685    }
686
687=head2 Server-provided dynamic value for field
688
689There are many different ways to provide values for fields. Default values can be
690statically provided in the form with the 'default' attribute on the field, with
691a default_<field_name> method in the form, with an init_object/item, and with
692'default_over_obj' if you have both an item/init_object and want to provide a
693default.
694
695    has_field 'foo' => ( default => 'my_default' );
696    has_field 'foo' => ( default_over_obj => 'my_default' );
697    sub default_foo { 'my_default' }
698    ..
699    $form->process( init_object => { foo => 'my_default } );
700    $form->process( item => <object with $obj->foo method to provide default> );
701
702If you want to change the default for the field at run time, there are a number
703of options.
704
705You can set the value in the init_object or item before doing process:
706
707    my $foo_value = 'some calculated value';
708    $form->process( init_object => { foo => $foo_value } );
709
710You can use 'update_field_list' or 'defaults' on the 'process' call:
711
712    $form->process( update_field_list => { foo => { default => $foo_value } } );
713    -- or --
714    $form->process( defaults => { foo => $foo_value } );
715
716You can set a Moose attribute in the form class, and set the default in a
717default_<field_name> method:
718
719    package My::Form;
720    use HTML::FormHandler::Moose;
721    extends 'HTML::Formhandler';
722
723    has 'form_id' => ( isa => 'Str', is => 'rw' );
724    has_field 'foo';
725    sub default_foo {
726        my $self = shift;
727        return $self->form_id;
728    }
729    ....
730    $form->process( form_id => 'my_form', params => $params );
731
732You can set a Moose attribute in the form class and set it in an update_fields
733method:
734
735    sub update_fields {
736        my $self = shift;
737        $self->field('foo')->default('my_form');
738    }
739
740=head2 Static form, dynamic field IDs
741
742The problem: you have a form that will be used in multiple places on a page, but you
743want to use a static form instead of doing 'new' for each. You can pass a form name in
744on the process call and use 'html_prefix' in the form:
745
746   $form->process( name => '...', params => {} );
747
748But the field 'id' attribute has already been constructed and doesn't change.
749
750Solution: apply a role to the base field class to replace the 'id' getter for the 'id'
751attribute with a method which constructs the 'id' dynamically. Since the role is
752being applied to the base field class, you can't just use 'sub id', because the
753'id' method defined by the 'id' attribute has precedence. So create an 'around'
754method modifier that replaces it in the role.
755
756    package My::DynamicFieldId;
757    use Moose::Role;
758    around 'id' => sub {
759        my $orig = shift;
760        my $self = shift;
761        my $form_name = $self->form->name;
762        return $form_name . "." . $self->full_name;
763    };
764
765    package My::CustomIdForm;
766    use HTML::FormHandler::Moose;
767    extends 'HTML::FormHandler';
768
769    has '+html_prefix' => ( default => 1 );
770    has '+field_traits' => ( default => sub { ['My::DynamicFieldId'] } );
771
772    has_field 'foo';
773    has_field 'bar';
774
775=head2 Create different field IDs
776
777Use 'build_id_method' to give your fields a different format 'id':
778
779    package MyApp::CustomId;
780    use HTML::FormHandler::Moose;
781    extends 'HTML::FormHandler';
782
783    has '+update_field_list' => ( default =>
784        sub { { all => { build_id_method => \&custom_id } } } );
785    has_field 'foo' => ( type => 'Compound' );
786    has_field 'foo.one';
787    has_field 'foo.two';
788    has_field 'foo.three';
789    sub custom_id {
790        my $self = shift;
791        my $full_name = $self->full_name;
792        $full_name =~ s/\./_/g;
793        return $full_name;
794    }
795
796The above method provides IDs of "foo_two" and "foo_three" instead of
797"foo.two" and "foo.three".
798
799=cut