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