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