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