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