Fix typo in Intro.pod & add new header to AdvancedCRUD.pod
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / AdvancedCRUD.pod
CommitLineData
d442cc9f 1=head1 NAME
2
50211f3c 3Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 9: Advanced CRUD
d442cc9f 4
5
6=head1 OVERVIEW
7
50211f3c 8This is B<Part 9 of 10> for the Catalyst tutorial.
d442cc9f 9
10L<Tutorial Overview|Catalyst::Manual::Tutorial>
11
12=over 4
13
14=item 1
15
16L<Introduction|Catalyst::Manual::Tutorial::Intro>
17
18=item 2
19
20L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics>
21
22=item 3
23
50211f3c 24L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics>
d442cc9f 25
26=item 4
27
50211f3c 28L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD>
d442cc9f 29
30=item 5
31
50211f3c 32L<Authentication|Catalyst::Manual::Tutorial::Authentication>
d442cc9f 33
34=item 6
35
50211f3c 36L<Authorization|Catalyst::Manual::Tutorial::Authorization>
d442cc9f 37
38=item 7
39
50211f3c 40L<Debugging|Catalyst::Manual::Tutorial::Debugging>
d442cc9f 41
42=item 8
43
50211f3c 44L<Testing|Catalyst::Manual::Tutorial::Testing>
d442cc9f 45
46=item 9
47
50211f3c 48B<Advanced CRUD>
49
50=item 10
51
d442cc9f 52L<Appendices|Catalyst::Manual::Tutorial::Appendices>
53
54=back
55
50211f3c 56
d442cc9f 57=head1 DESCRIPTION
58
59This part of the tutorial explores more advanced functionality for
60Create, Read, Update, and Delete (CRUD) than we saw in Part 3. In
61particular, it looks at a number of techniques that can be useful for
62the Update portion of CRUD, such as automated form generation,
63validation of user-entered data, and automated transfer of data between
64forms and model objects.
65
66In keeping with the Catalyst (and Perl) spirit of flexibility, there are
67many different ways to approach advanced CRUD operations in a Catalyst
68environment. One alternative is to use
69L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold>
70to instantly construct a set of Controller methods and templates for
71basic CRUD operations. Although a popular subject in Quicktime
72movies that serve as promotional material for various frameworks,
73real-world applications generally require more control. Other
74options include L<Data::FormValidator|Data::FormValidator> and
75L<HTML::FillInForm|HTML::FillInForm>.
76
2d0526d1 77Note that HTML::Widget is no longer maintained.
78L<HTML::FormFu|HTML::FormFu> was developed as a replacement. There is
79an example HTML::FormFu application at
80L<http://dev.catalyst.perl.org/repos/Catalyst/examples/Advent07FormFu/final/Fu/Fu-0.01.tar.gz>.
81Another popular alternative for HTML FormFu is
82L<Catalyst::Controller::Formbuilder|Catalyst::Controller::Formbuilder>
83which is used in the L<Catalyst
84Book|http://www.packtpub.com/catalyst-perl-web-application/book>.
85
86Here, we will make use of the
87L<HTML::Widget|HTML::Widget> to not only ease form creation, but to
88also provide validation of the submitted data. The approached used by
89this part of the tutorial is to slowly incorporate additional
90L<HTML::Widget|HTML::Widget> functionality in a step-wise fashion (we
91start with fairly simple form creation and then move on to more
92complex and "magical" features such as validation and
d442cc9f 93auto-population/auto-saving).
94
95B<Note:> Part 8 of the tutorial is optional. Users who do not wish to
96use L<HTML::Widget|HTML::Widget> may skip this part.
97
98You 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
102This section looks at how L<HTML::Widget|HTML::Widget> can be used to
103add additional functionality to the manually created form from Part 3.
104
105=head2 Add the C<HTML::Widget> Plugin
106
107Open C<lib/MyApp.pm> in your editor and add the following to the list of
108plugins (be sure to leave the existing plugins enabled):
109
110 HTML::Widget
111
112=head2 Add a Form Creation Helper Method
113
114Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
115following 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
145This method provides a central location that builds an
146HTML::Widget-based form with the appropriate fields. The "Get authors"
147code uses DBIC to retrieve a list of model objects and then uses C<map>
148to create a hash where the hash keys are the database primary keys from
149the authors table and the associated values are the last names of the
150authors.
151
152=head2 Add Actions to Display and Save the Form
153
154Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
155following 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
211Note how we use C<make_book_widget> to build the core parts of the form
212in one location, but we set the action (the URL the form is sent to when
213the user clicks the 'Submit' button) separately in C<hw_create>. Doing
214so 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
216update an existing book object).
217
218B<NOTE:> If you receive an error about Catalyst not being able to find
219the template C<hw_create_do.tt2>, please verify that you followed the
220instructions in the final section of
221L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> where
222you returned to a manually-specified template. You can either use
223C<forward>/C<detach> B<OR> default template names, but the two cannot
224be used together.
225
226
227=head2 Update the CSS
228
229Edit C<root/src/ttsite.css> and add the following lines to the bottom of
230the 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
253These changes will display form elements vertically and also show error
254messages in red. Note that we are pulling the color scheme settings
255from the C<root/lib/config/col> file that was created by the TTSite
256helper. This allows us to change the color used by various error styles
257in the CSS from a single location.
258
259=head2 Create a Template Page To Display The Form
260
261Open 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
271Open C<root/src/books/list.tt2> in your editor and add the following to
272the 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
282Press C<Ctrl-C> to kill the previous server instance (if it's still
283running) and restart it:
284
285 $ script/myapp_server.pl
286
287Login 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
289out the form with the following values: Title = "Internetworking with
290TCP/IP Vol. II", Rating = "4", and Author = "Comer". Click Submit, and
291you will be returned to the Create/Update Book page with a "Book
292created" status message displayed. Click "Return to book list" to view
293the newly created book on the main list.
294
295Also note that this implementation allows you to can create books with
296bogus information. Although we have constrained the authors with the
297drop-down list, there are no restrictions on items such as the length of
298the title (for example, you can create a one-letter title) and value for
299the rating (you can use any number you want, and even non-numeric values
300with SQLite). The next section will address this concern.
301
302B<Note:> Depending on the database you are using and how you established
303the columns in your tables, the database could obviously provide various
304levels of "type enforcement" on your data. The key point being made in
305the previous paragraph is that the I<web application> itself is not
306performing any validation.
307
308=head1 C<HTML::WIDGET> VALIDATION AND FILTERING
309
310Although the use of L<HTML::Widget|HTML::Widget> in the previous section
311did provide an automated mechanism to build the form, the real power of
312this module stems from functionality that can automatically validate and
313filter the user input. Validation uses constraints to be sure that
314users input appropriate data (for example, that the email field of a
315form contains a valid email address). Filtering can be used to remove
316extraneous whitespace from fields or to escape meta-characters in user
317input.
318
319=head2 Add Constraints and Filters to the Widget Creation Method
320
321Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
322C<make_book_widget> method to match the following (new sections have
323been 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
364The main changes are:
365
366=over 4
367
368=item *
369
370The C<Select> element for C<authors> is changed from a single-select
371drop-down to a multi-select list by adding calls to C<multiple> (set to
372C<true>) and C<size> (set to the number of rows to display).
373
374=item *
375
376Four sets of constraints are added to provide validation of the user input.
377
378=item *
379
380Two 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
386Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
387match the following code (enough of the code is different that you
388probably 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
439The key changes to C<hw_create_do> are:
440
441=over 4
442
443=item *
444
445C<hw_create_do> no longer does a C<detach> to C<hw_create> to redisplay
446the form. Now that C<hw_create_do> has to process the form in order to
447perform the validation, we go ahead and build a complete set of form
448presentation logic into C<hw_create_do> (for example, C<hw_create_do>
449now has a C<$c-E<gt>stash-E<gt>{template}> line). Note that if we
450process 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
452twice, resulting in a duplicate set of elements being added to the form.
453(There are other ways to address the "duplicate form rendering" issue --
454just be aware that it exists.)
455
456=item *
457
458C<$w-E<gt>process($c-E<gt>req)> is called to run the validation logic.
459Not only does this set the C<has_errors> flag if validation errors are
460encountered, it returns a string containing any field-specific warning
461messages.
462
463=item *
464
465An C<if> statement checks if any validation errors were encountered. If
466so, C<$c-E<gt>stash-E<gt>{error_msg}> is set and the input form is
467redisplayed. If no errors were found, the object is created in a manner
468similar to the prior version of the C<hw_create_do> method.
469
470=back
471
472=head2 Try Out the Form
473
474Press C<Ctrl-C> to kill the previous server instance (if it's still
475running) and restart it:
476
477 $ script/myapp_server.pl
478
479Now try adding a book with various errors: title less than 5 characters,
480non-numeric rating, a rating of 0 or 6, etc. Also try selecting one,
481two, and zero authors. When you click Submit, the HTML::Widget
482C<constraint> items will validate the logic and insert feedback as
483appropriate.
484
485
486=head1 Enable C<DBIx::Class::HTMLWidget> Support
487
488In this section we will take advantage of some of the "auto-population"
489features of C<DBIx::Class::HTMLWidget>. Enabling
490C<DBIx::Class::HTMLWidget> provides two additional methods to your DBIC
491model classes:
492
493=over 4
494
495=item *
496
497fill_widget()
498
499Takes data from the database and transfers it to your form widget.
500
501=item *
502
503populate_from_widget()
504
505Takes data from a form widget and uses it to update the corresponding
506records in the database.
507
508=back
509
510In other words, the two methods are a mirror image of each other: one
511reads from the database while the other writes to the database.
512
513=head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
514
515In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
516need to add C<HTMLWidget> to the C<load_components> line of DBIC result
517source files that need to use the C<fill_widget> and
518C<populate_from_widget> methods. In this case, open
519C<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
525Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
526match 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
578In this version of C<hw_create_do> we removed the logic that manually
579pulled the form variables and used them to call
580C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
581single call to C<$book-E<gt>populate_from_widget>. Note that we still
582have to call C<$book-E<gt>add_to_book_authors> once per author because
583C<populate_from_widget> does not currently handle the relationships
584between tables. Also, we reset the form to an empty fields by adding
585another 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)>,
587the form values already entered will be retained on redisplay --
588although this could be desirable for some applications, we avoid it
589here to help avoid the creation of duplicate records).
590
591
592=head2 Try Out the Form
593
594Press C<Ctrl-C> to kill the previous server instance (if it's still
595running) and restart it:
596
597 $ script/myapp_server.pl
598
599Try adding a book that validates. Return to the book list and the book
600you added should be visible.
601
602
603
604=head1 Rendering C<HTMLWidget> Forms in a Table
605
606Some developers my wish to use the "old-fashioned" table style of
607rendering a form in lieu of the default C<HTML::Widget> rendering that
608assumes you will use CSS for formatting. This section demonstrates
609some techniques that can override the default rendering with a
610custom class.
611
612
613=head2 Add a New "Element Container"
614
615Open 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
637This simply dumps the HTML code for a given form element, followed by a
638C<span> that can contain validation error message.
639
640
641=head2 Enable the New Element Container When Building the Form
642
643Open C<lib/MyApp/Controller/Books.pm> in your editor. First add a
644C<use> for your element container class:
645
646 use FormElementContainer;
647
648B<Note:> If you forget to C<use> your container class in your
649controller, then your form will not be displayed and no error messages
650will be generated. Don't forget this important step!
651
652Then tell C<HTML::Widget> to use that class during rendering by updating
653C<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
697The two new lines are marked with C<***New:>.
698
699
700=head2 Update the TT Template
701
702Open 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
738This represents three changes:
739
740=over 4
741
742=item *
743
744The existing C<widget_result.as_xml> has been commented out.
745
746=item *
747
748It loops through each form element, displaying the field name in the
749first table cell along with the form element and validation errors in
750the second field.
751
752=item *
753
754JavaScript 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
761Press C<Ctrl-C> to kill the previous server instance (if it's still
762running) and restart it:
763
764 $ script/myapp_server.pl
765
766Try adding a book that validates. Return to the book list and the book
767you added should be visible.
768
769
770=head1 AUTHOR
771
772Kennedy Clark, C<hkclark@gmail.com>
773
774Please report any errors, issues or suggestions to the author. The
775most recent version of the Catalyst Tutorial can be found at
d712b826 776L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
d442cc9f 777
778Copyright 2006, Kennedy Clark, under Creative Commons License
779(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).