Fix typo in Intro.pod & add new header to AdvancedCRUD.pod
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / AdvancedCRUD.pod
1 =head1 NAME
2
3 Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 9: Advanced CRUD
4
5
6 =head1 OVERVIEW
7
8 This is B<Part 9 of 10> for the Catalyst tutorial.
9
10 L<Tutorial Overview|Catalyst::Manual::Tutorial>
11
12 =over 4
13
14 =item 1
15
16 L<Introduction|Catalyst::Manual::Tutorial::Intro>
17
18 =item 2
19
20 L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics>
21
22 =item 3
23
24 L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics>
25
26 =item 4
27
28 L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD>
29
30 =item 5
31
32 L<Authentication|Catalyst::Manual::Tutorial::Authentication>
33
34 =item 6
35
36 L<Authorization|Catalyst::Manual::Tutorial::Authorization>
37
38 =item 7
39
40 L<Debugging|Catalyst::Manual::Tutorial::Debugging>
41
42 =item 8
43
44 L<Testing|Catalyst::Manual::Tutorial::Testing>
45
46 =item 9
47
48 B<Advanced CRUD>
49
50 =item 10
51
52 L<Appendices|Catalyst::Manual::Tutorial::Appendices>
53
54 =back
55
56
57 =head1 DESCRIPTION
58
59 This part of the tutorial explores more advanced functionality for
60 Create, Read, Update, and Delete (CRUD) than we saw in Part 3.  In
61 particular, it looks at a number of techniques that can be useful for
62 the Update portion of CRUD, such as automated form generation,
63 validation of user-entered data, and automated transfer of data between
64 forms and model objects.
65
66 In keeping with the Catalyst (and Perl) spirit of flexibility, there are
67 many different ways to approach advanced CRUD operations in a Catalyst
68 environment.  One alternative is to use
69 L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold> 
70 to instantly construct a set of Controller methods and templates for 
71 basic CRUD operations.  Although a popular subject in Quicktime 
72 movies that serve as promotional material for various frameworks, 
73 real-world applications generally require more control.  Other 
74 options include L<Data::FormValidator|Data::FormValidator> and
75 L<HTML::FillInForm|HTML::FillInForm>.
76
77 Note that HTML::Widget is no longer maintained.
78 L<HTML::FormFu|HTML::FormFu> was developed as a replacement.  There is
79 an example HTML::FormFu application at
80 L<http://dev.catalyst.perl.org/repos/Catalyst/examples/Advent07FormFu/final/Fu/Fu-0.01.tar.gz>.
81 Another popular alternative for HTML FormFu is
82 L<Catalyst::Controller::Formbuilder|Catalyst::Controller::Formbuilder>
83 which is used in the L<Catalyst
84 Book|http://www.packtpub.com/catalyst-perl-web-application/book>.
85
86 Here, we will make use of the
87 L<HTML::Widget|HTML::Widget> to not only ease form creation, but to
88 also provide validation of the submitted data.  The approached used by
89 this part of the tutorial is to slowly incorporate additional
90 L<HTML::Widget|HTML::Widget> functionality in a step-wise fashion (we
91 start with fairly simple form creation and then move on to more
92 complex and "magical" features such as validation and
93 auto-population/auto-saving).
94
95 B<Note:> Part 8 of the tutorial is optional.  Users who do not wish to
96 use L<HTML::Widget|HTML::Widget> may skip this part.
97
98 You can checkout the source code for this example from the catalyst subversion repository as per the instructions in L<Catalyst::Manual::Tutorial::Intro>
99
100 =head1 C<HTML::WIDGET> FORM CREATION
101
102 This section looks at how L<HTML::Widget|HTML::Widget> can be used to
103 add additional functionality to the manually created form from Part 3.
104
105 =head2 Add the C<HTML::Widget> Plugin
106
107 Open C<lib/MyApp.pm> in your editor and add the following to the list of
108 plugins (be sure to leave the existing plugins enabled):
109
110     HTML::Widget
111
112 =head2 Add a Form Creation Helper Method
113
114 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
115 following method:
116
117     =head2 make_book_widget
118     
119     Build an HTML::Widget form for book creation and updates
120     
121     =cut
122     
123     sub make_book_widget {
124         my ($self, $c) = @_;
125     
126         # Create an HTML::Widget to build the form
127         my $w = $c->widget('book_form')->method('post');
128     
129         # Get authors
130         my @authorObjs = $c->model("MyAppDB::Author")->all();
131         my @authors = map {$_->id => $_->last_name }
132                            sort {$a->last_name cmp $b->last_name} @authorObjs;
133     
134         # Create the form feilds
135         $w->element('Textfield', 'title'  )->label('Title')->size(60);
136         $w->element('Textfield', 'rating' )->label('Rating')->size(1);
137         $w->element('Select',    'authors')->label('Authors')
138             ->options(@authors);
139         $w->element('Submit',    'submit' )->value('submit');
140     
141         # Return the widget    
142         return $w;
143     }
144
145 This method provides a central location that builds an 
146 HTML::Widget-based form with the appropriate fields.  The "Get authors" 
147 code uses DBIC to retrieve a list of model objects and then uses C<map> 
148 to create a hash where the hash keys are the database primary keys from 
149 the authors table and the associated values are the last names of the 
150 authors.
151
152 =head2 Add Actions to Display and Save the Form
153
154 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
155 following methods:
156
157     =head2 hw_create
158     
159     Build an HTML::Widget form for book creation and updates
160     
161     =cut
162     
163     sub hw_create : Local {
164         my ($self, $c) = @_;
165     
166         # Create the widget and set the action for the form
167         my $w = $self->make_book_widget($c);
168         $w->action($c->uri_for('hw_create_do'));
169     
170         # Write form to stash variable for use in template
171         $c->stash->{widget_result} = $w->result;
172     
173         # Set the template
174         $c->stash->{template} = 'books/hw_form.tt2';
175     }
176     
177     
178     =head2 hw_create_do
179     
180     Build an HTML::Widget form for book creation and updates
181     
182     =cut
183     
184     sub hw_create_do : Local {
185         my ($self, $c) = @_;
186     
187         # Retrieve the data from the form
188         my $title   = $c->request->params->{title};
189         my $rating  = $c->request->params->{rating};
190         my $authors = $c->request->params->{authors};
191     
192         # Call create() on the book model object. Pass the table 
193         # columns/field values we want to set as hash values
194         my $book = $c->model('MyAppDB::Book')->create({
195                 title   => $title,
196                 rating  => $rating
197             });
198         
199         # Add a record to the join table for this book, mapping to 
200         # appropriate author
201         $book->add_to_book_authors({author_id => $authors});
202         
203         # Set a status message for the user
204         $c->stash->{status_msg} = 'Book created';
205     
206         # Use 'hw_create' to redisplay the form.  As discussed in 
207         # Part 3, 'detach' is like 'forward', but it does not return
208         $c->detach('hw_create');
209     }
210
211 Note how we use C<make_book_widget> to build the core parts of the form
212 in one location, but we set the action (the URL the form is sent to when
213 the user clicks the 'Submit' button) separately in C<hw_create>.  Doing
214 so allows us to have the same form submit the data to different actions
215 (e.g., C<hw_create_do> for a create operation but C<hw_update_do> to
216 update an existing book object).
217
218 B<NOTE:> If you receive an error about Catalyst not being able to find
219 the template C<hw_create_do.tt2>, please verify that you followed the
220 instructions in the final section of
221 L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> where
222 you returned to a manually-specified template.  You can either use 
223 C<forward>/C<detach> B<OR> default template names, but the two cannot
224 be used together.
225
226
227 =head2 Update the CSS
228
229 Edit C<root/src/ttsite.css> and add the following lines to the bottom of
230 the file:
231
232     label {
233         display: block;
234         width: 10em;
235         position: relative;
236         margin: .5em 0em;
237     }
238     label input {
239         position: absolute;
240         left: 100%;
241     }
242     label select {
243         position: absolute;
244         left: 100%;
245     }
246     .submit {
247         margin-top: 2em;;
248     }
249     .error_messages {
250         color: [% site.col.error %];
251     }
252
253 These changes will display form elements vertically and also show error
254 messages in red.  Note that we are pulling the color scheme settings
255 from the C<root/lib/config/col> file that was created by the TTSite
256 helper.  This allows us to change the color used by various error styles
257 in the CSS from a single location.
258
259 =head2 Create a Template Page To Display The Form
260
261 Open C<root/src/books/hw_form.tt2> in your editor and enter the following:
262
263     [% META title = 'Create/Update Book' %]
264     
265     [% widget_result.as_xml %]
266     
267     <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
268
269 =head2 Add Links for Create and Update via C<HTML::Widget>
270
271 Open C<root/src/books/list.tt2> in your editor and add the following to
272 the bottom of the existing file:
273
274     <p>
275       HTML::Widget:
276       <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
277     </p>
278
279
280 =head2 Test The <HTML::Widget> Create Form
281
282 Press C<Ctrl-C> to kill the previous server instance (if it's still
283 running) and restart it:
284
285     $ script/myapp_server.pl
286
287 Login as C<test01>.  Once at the Book List page, click the HTML::Widget
288 "Create" link to display for form produced by C<make_book_widget>.  Fill
289 out the form with the following values: Title = "Internetworking with
290 TCP/IP Vol. II", Rating = "4", and Author = "Comer".  Click Submit, and
291 you will be returned to the Create/Update Book page with a "Book
292 created" status message displayed.  Click "Return to book list" to view
293 the newly created book on the main list.
294
295 Also note that this implementation allows you to can create books with
296 bogus information.  Although we have constrained the authors with the
297 drop-down list, there are no restrictions on items such as the length of
298 the title (for example, you can create a one-letter title) and value for
299 the rating (you can use any number you want, and even non-numeric values
300 with SQLite).  The next section will address this concern.
301
302 B<Note:> Depending on the database you are using and how you established
303 the columns in your tables, the database could obviously provide various
304 levels of "type enforcement" on your data.  The key point being made in
305 the previous paragraph is that the I<web application> itself is not
306 performing any validation.
307
308 =head1 C<HTML::WIDGET> VALIDATION AND FILTERING
309
310 Although the use of L<HTML::Widget|HTML::Widget> in the previous section
311 did provide an automated mechanism to build the form, the real power of
312 this module stems from functionality that can automatically validate and
313 filter the user input.  Validation uses constraints to be sure that
314 users input appropriate data (for example, that the email field of a
315 form contains a valid email address).  Filtering can be used to remove
316 extraneous whitespace from fields or to escape meta-characters in user
317 input.
318
319 =head2 Add Constraints and Filters to the Widget Creation Method
320
321 Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
322 C<make_book_widget> method to match the following (new sections have
323 been marked with a C<*** NEW:> comment):
324
325     sub make_book_widget {
326         my ($self, $c) = @_;
327     
328         # Create an HTML::Widget to build the form
329         my $w = $c->widget('book_form')->method('post');
330             
331         # Get authors
332         my @authorObjs = $c->model("MyAppDB::Author")->all();
333         my @authors = map {$_->id => $_->last_name }
334                            sort {$a->last_name cmp $b->last_name} @authorObjs;
335     
336         # Create the form feilds
337         $w->element('Textfield', 'title'  )->label('Title')->size(60);
338         $w->element('Textfield', 'rating' )->label('Rating')->size(1);
339         # ***NEW: Convert to multi-select list
340         $w->element('Select',    'authors')->label('Authors')
341             ->options(@authors)->multiple(1)->size(3);
342         $w->element('Submit',    'submit' )->value('submit');
343     
344         # ***NEW: Set constraints
345         $w->constraint(All     => qw/title rating authors/)
346             ->message('Required. ');
347         $w->constraint(Integer => qw/rating/)
348             ->message('Must be an integer. ');
349         $w->constraint(Range   => qw/rating/)->min(1)->max(5)
350             ->message('Must be a number between 1 and 5. ');
351         $w->constraint(Length  => qw/title/)->min(5)->max(50)
352             ->message('Must be between 5 and 50 characters. ');
353     
354         # ***NEW: Set filters
355         for my $column (qw/title rating authors/) {
356             $w->filter( HTMLEscape => $column );
357             $w->filter( TrimEdges  => $column );
358         }
359     
360         # Return the widget    
361         return $w;
362     }
363
364 The main changes are:
365
366 =over 4
367
368 =item *
369
370 The C<Select> element for C<authors> is changed from a single-select
371 drop-down to a multi-select list by adding calls to C<multiple> (set to
372 C<true>) and C<size> (set to the number of rows to display).
373
374 =item *
375
376 Four sets of constraints are added to provide validation of the user input.
377
378 =item *
379
380 Two filters are run on every field to remove and escape unwanted input.
381
382 =back
383
384 =head2 Rebuild the Form Submission Method to Include Validation
385
386 Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
387 match the following code (enough of the code is different that you
388 probably want to cut and paste this over code the existing method):
389
390     sub hw_create_do : Local {
391         my ($self, $c) = @_;
392     
393         # Retrieve the data from the form
394         my $title   = $c->request->params->{title};
395         my $rating  = $c->request->params->{rating};
396         my $authors = $c->request->params->{authors};
397         
398         # Create the widget and set the action for the form
399         my $w = $self->make_book_widget($c);
400         $w->action($c->uri_for('hw_create_do'));
401     
402         # Validate the form parameters
403         my $result = $w->process($c->req);
404     
405         # Write form (including validation error messages) to
406         # stash variable for use in template
407         $c->stash->{widget_result} = $result;
408     
409         # Were their validation errors?
410         if ($result->has_errors) {
411             # Warn the user at the top of the form that there were errors.
412             # Note that there will also be per-field feedback on
413             # validation errors because of '$w->process($c->req)' above.
414             $c->stash->{error_msg} = 'Validation errors!';
415         } else {
416             # Everything validated OK, so do the create
417             # Call create() on the book model object. Pass the table
418             # columns/field values we want to set as hash values
419             my $book = $c->model('MyAppDB::Book')->create({
420                     title   => $title,
421                     rating  => $rating
422                 });
423     
424             # Add a record to the join table for this book, mapping to
425             # appropriate author.  Note that $authors will be 1 author as
426             # a scalar or ref to list of authors depending on how many the
427             # user selected; the 'ref $authors ?...' handles both cases
428             foreach my $author (ref $authors ? @$authors : $authors) {
429                 $book->add_to_book_authors({author_id => $author});
430             }    
431             # Set a status message for the user
432             $c->stash->{status_msg} = 'Book created';
433         }
434     
435         # Set the template
436         $c->stash->{template} = 'books/hw_form.tt2';
437     }
438
439 The key changes to C<hw_create_do> are:
440
441 =over 4
442
443 =item *
444
445 C<hw_create_do> no longer does a C<detach> to C<hw_create> to redisplay
446 the form.  Now that C<hw_create_do> has to process the form in order to
447 perform the validation, we go ahead and build a complete set of form
448 presentation logic into C<hw_create_do> (for example, C<hw_create_do>
449 now has a C<$c-E<gt>stash-E<gt>{template}> line).  Note that if we
450 process the form in C<hw_create_do> I<and> forward/detach back to
451 <hw_create>, we would end up with C<make_book_widget> being called
452 twice, resulting in a duplicate set of elements being added to the form.
453 (There are other ways to address the "duplicate form rendering" issue --
454 just be aware that it exists.)
455
456 =item *
457
458 C<$w-E<gt>process($c-E<gt>req)> is called to run the validation logic.
459 Not only does this set the C<has_errors> flag if validation errors are
460 encountered, it returns a string containing any field-specific warning
461 messages.
462
463 =item *
464
465 An C<if> statement checks if any validation errors were encountered.  If
466 so, C<$c-E<gt>stash-E<gt>{error_msg}> is set and the input form is
467 redisplayed.  If no errors were found, the object is created in a manner
468 similar to the prior version of the C<hw_create_do> method.
469
470 =back
471
472 =head2 Try Out the Form
473
474 Press C<Ctrl-C> to kill the previous server instance (if it's still 
475 running) and restart it:
476
477     $ script/myapp_server.pl
478
479 Now try adding a book with various errors: title less than 5 characters,
480 non-numeric rating, a rating of 0 or 6, etc.  Also try selecting one,
481 two, and zero authors.  When you click Submit, the HTML::Widget
482 C<constraint> items will validate the logic and insert feedback as
483 appropriate.
484
485
486 =head1 Enable C<DBIx::Class::HTMLWidget> Support
487
488 In this section we will take advantage of some of the "auto-population"
489 features of C<DBIx::Class::HTMLWidget>.  Enabling
490 C<DBIx::Class::HTMLWidget> provides two additional methods to your DBIC
491 model classes:
492
493 =over 4
494
495 =item *
496
497 fill_widget()
498
499 Takes data from the database and transfers it to your form widget.
500
501 =item *
502
503 populate_from_widget()
504
505 Takes data from a form widget and uses it to update the corresponding
506 records in the database.
507
508 =back
509
510 In other words, the two methods are a mirror image of each other: one
511 reads from the database while the other writes to the database.
512
513 =head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
514
515 In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
516 need to add C<HTMLWidget> to the C<load_components> line of DBIC result
517 source files that need to use the C<fill_widget> and
518 C<populate_from_widget> methods.  In this case, open
519 C<lib/MyAppDB/Book.pm> and update the C<load_components> line to match:
520
521         __PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
522
523 =head2 Use C<populate_from_widget> in C<hw_create_do>
524
525 Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
526 match the following code:
527
528     =head2 hw_create_do
529     
530     Build an HTML::Widget form for book creation and updates
531     
532     =cut
533     
534     sub hw_create_do : Local {
535         my ($self, $c) = @_;
536     
537         # Create the widget and set the action for the form
538         my $w = $self->make_book_widget($c);
539         $w->action($c->uri_for('hw_create_do'));
540     
541         # Validate the form parameters
542         my $result = $w->process($c->req);
543     
544         # Write form (including validation error messages) to
545         # stash variable for use in template
546         $c->stash->{widget_result} = $result;
547     
548         # Were their validation errors?
549         if ($result->has_errors) {
550             # Warn the user at the top of the form that there were errors.
551             # Note that there will also be per-field feedback on
552             # validation errors because of '$w->process($c->req)' above.
553             $c->stash->{error_msg} = 'Validation errors!';
554         } else {
555             my $book = $c->model('MyAppDB::Book')->new({});
556             $book->populate_from_widget($result);
557     
558             # Add a record to the join table for this book, mapping to
559             # appropriate author.  Note that $authors will be 1 author as
560             # a scalar or ref to list of authors depending on how many the
561             # user selected; the 'ref $authors ?...' handles both cases
562             my $authors = $c->request->params->{authors};
563             foreach my $author (ref $authors ? @$authors : $authors) {
564                 $book->add_to_book_authors({author_id => $author});
565             }
566     
567             # Set a status message for the user
568             $c->flash->{status_msg} = 'Book created';
569             
570             # Redisplay an empty form for another
571             $c->stash->{widget_result} = $w->result;
572         }
573     
574         # Set the template
575         $c->stash->{template} = 'books/hw_form.tt2';
576     }
577
578 In this version of C<hw_create_do> we removed the logic that manually
579 pulled the form variables and used them to call
580 C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
581 single call to C<$book-E<gt>populate_from_widget>.  Note that we still
582 have to call C<$book-E<gt>add_to_book_authors> once per author because
583 C<populate_from_widget> does not currently handle the relationships
584 between tables.  Also, we reset the form to an empty fields by adding
585 another call to C<$w-E<gt>result> and storing the output in the stash 
586 (if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
587 the form values already entered will be retained on redisplay --
588 although this could be desirable for some applications, we avoid it
589 here to help avoid the creation of duplicate records).
590
591
592 =head2 Try Out the Form
593
594 Press C<Ctrl-C> to kill the previous server instance (if it's still 
595 running) and restart it:
596
597     $ script/myapp_server.pl
598
599 Try adding a book that validates.  Return to the book list and the book 
600 you added should be visible.
601
602
603
604 =head1 Rendering C<HTMLWidget> Forms in a Table
605
606 Some developers my wish to use the "old-fashioned" table style of 
607 rendering a form in lieu of the default C<HTML::Widget> rendering that 
608 assumes you will use CSS for formatting.  This section demonstrates
609 some techniques that can override the default rendering with a 
610 custom class.
611
612
613 =head2 Add a New "Element Container"
614
615 Open C<lib/FormElementContainer.pm> in your editor and enter:
616
617     package FormElementContainer;
618     
619     use base 'HTML::Widget::Container';
620     
621     sub _build_element {
622         my ($self, $element) = @_;
623     
624         return () unless $element;
625         if (ref $element eq 'ARRAY') {
626             return map { $self->_build_element($_) } @{$element};
627         }
628         my $e = $element->clone;
629         $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
630             if $self->error && $e->tag eq 'input';
631     
632         return $e ? ($e) : ();
633     }
634     
635     1;
636
637 This simply dumps the HTML code for a given form element, followed by a 
638 C<span> that can contain validation error message.
639
640
641 =head2 Enable the New Element Container When Building the Form
642
643 Open C<lib/MyApp/Controller/Books.pm> in your editor.  First add a
644 C<use> for your element container class:
645
646     use FormElementContainer;
647
648 B<Note:> If you forget to C<use> your container class in your 
649 controller, then your form will not be displayed and no error messages 
650 will be generated. Don't forget this important step!
651
652 Then tell C<HTML::Widget> to use that class during rendering by updating
653 C<make_book_widget> to match the following:
654
655     sub make_book_widget {
656         my ($self, $c) = @_;
657     
658         # Create an HTML::Widget to build the form
659         my $w = $c->widget('book_form')->method('post');
660     
661         # ***New: Use custom class to render each element in the form    
662         $w->element_container_class('FormElementContainer');
663         
664         # Get authors
665         my @authorObjs = $c->model("MyAppDB::Author")->all();
666         my @authors = map {$_->id => $_->last_name }
667                            sort {$a->last_name cmp $b->last_name} @authorObjs;
668     
669         # Create the form feilds
670         $w->element('Textfield', 'title'  )->label('Title')->size(60);
671         $w->element('Textfield', 'rating' )->label('Rating')->size(1);
672         # Convert to multi-select list
673         $w->element('Select',    'authors')->label('Authors')
674             ->options(@authors)->multiple(1)->size(3);
675         $w->element('Submit',    'submit' )->value('submit');
676     
677         # Set constraints
678         $w->constraint(All     => qw/title rating authors/)
679             ->message('Required. ');
680         $w->constraint(Integer => qw/rating/)
681             ->message('Must be an integer. ');
682         $w->constraint(Range   => qw/rating/)->min(1)->max(5)
683             ->message('Must be a number between 1 and 5. ');
684         $w->constraint(Length  => qw/title/)->min(5)->max(50)
685             ->message('Must be between 5 and 50 characters. ');
686     
687         # Set filters
688         for my $column (qw/title rating authors/) {
689             $w->filter( HTMLEscape => $column );
690             $w->filter( TrimEdges  => $column );
691         }
692     
693         # Return the widget    
694         return $w;
695     }
696
697 The two new lines are marked with C<***New:>.
698
699
700 =head2 Update the TT Template
701
702 Open C<root/src/books/hw_form.tt2> and edit it to match:
703
704     [% META title = 'Create/Update Book' %]
705     
706     [%# Comment out the auto-rendered form %]
707     [%# widget_result.as_xml %]
708     
709     
710     [%# Iterate over the form elements and display each -%]
711     <form name="book_form" action="[% widget_result.action %]" method="post">
712     <table border="0">
713     [% FOREACH element = widget_result.elements %]
714       <tr>
715         <td class="form-label">
716           [% element.label.as_text %]
717         </td>
718         <td class="form-element">
719           [% element.element_xml %]
720           <span class="form-error">
721             [% element.error_xml %]
722           </span>
723         </td>
724       </tr>
725     [% END %]
726     </table>
727     </form>
728     
729     
730     <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
731     
732     
733     [%# A little JavaScript to move the cursor to the first field %]
734     <script LANGUAGE="JavaScript">
735     document.book_form.book_form_title.focus();
736     </script>
737
738 This represents three changes:
739
740 =over 4
741
742 =item *
743
744 The existing C<widget_result.as_xml> has been commented out.
745
746 =item *
747
748 It loops through each form element, displaying the field name in the 
749 first table cell along with the form element and validation errors in 
750 the second field.
751
752 =item *
753
754 JavaScript to position the user's cursor in the first field of the form.
755
756 =back
757
758
759 =head2 Try Out the Form
760
761 Press C<Ctrl-C> to kill the previous server instance (if it's still 
762 running) and restart it:
763
764     $ script/myapp_server.pl
765
766 Try adding a book that validates.  Return to the book list and the book 
767 you added should be visible.
768
769
770 =head1 AUTHOR
771
772 Kennedy Clark, C<hkclark@gmail.com>
773
774 Please report any errors, issues or suggestions to the author.  The
775 most recent version of the Catalyst Tutorial can be found at
776 L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
777
778 Copyright 2006, Kennedy Clark, under Creative Commons License
779 (L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).