Suggestion from Ingo Wichmann.
[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
8112f931 73data. The approached used by this part of the tutorial is to slowly
be16bacd 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
7d310f12 86 svn co http://dev.catalyst.perl.org/repos/Catalyst/tags/examples/Tutorial/MyApp/5.7/AdvancedCRUD MyApp
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
7d310f12 133This method provides a central location that builds an
134HTML::Widget-based form with the appropriate fields. The "Get authors"
135code uses DBIC to retrieve a list of model objects and then uses C<map>
136to create a hash where the hash keys are the database primary keys from
137the authors table and the associated values are the last names of the
d3bfc796 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
8112f931 194 # Use 'hw_create' to redisplay the form. As discussed in
195 # Part 3, 'detach' is like 'forward', but it does not return
4d583dd8 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';
7d310f12 548
549 # Redisplay an empty form for another
550 $c->stash->{widget_result} = $w->result;
4d583dd8 551 }
552
553 # Set the template
554 $c->stash->{template} = 'books/hw_form.tt2';
555 }
556
64ccd8a8 557In this version of C<hw_create_do> we removed the logic that manually
558pulled the form variables and used them to call
559C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
560single call to C<$book-E<gt>populate_from_widget>. Note that we still
561have to call C<$book-E<gt>add_to_book_authors> once per author because
562C<populate_from_widget> does not currently handle the relationships
7d310f12 563between tables. Also, we reset the form to an empty fields by adding
564another call to C<$w-E<gt>result> and storing the output in the stash
565(if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
566the form values already entered will be retained on redisplay --
567although this could be desirable for some applications, we avoid it
568here to help avoid the creation of duplicate records).
4d583dd8 569
cc548726 570
785c4199 571=head2 Try Out the Form
572
573Press C<Ctrl-C> to kill the previous server instance (if it's still
574running) and restart it:
575
576 $ script/myapp_server.pl
577
578Try adding a book that validate. Return to the book list and the book
579you added should be visible.
580
581
582
583=head1 Rendering C<HTMLWidget> Forms in a Table
584
585Some developers my wish to use the "old-fashioned" table style of
586rendering a form in lieu of the default C<HTML::Widget> rendering that
7d310f12 587assumes you will use CSS for formatting. This section demonstrates
588some techniques that can override the default rendering with a
589custom class.
785c4199 590
591
592=head2 Add a New "Element Container"
593
594Open C<lib/FormElementContainer.pm> in your editor and enter:
595
596 package FormElementContainer;
597
598 use base 'HTML::Widget::Container';
599
600 sub _build_element {
601 my ($self, $element) = @_;
602
603 return () unless $element;
604 if (ref $element eq 'ARRAY') {
605 return map { $self->_build_element($_) } @{$element};
606 }
607 my $e = $element->clone;
785c4199 608 $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
609 if $self->error && $e->tag eq 'input';
610
611 return $e ? ($e) : ();
612 }
613
614 1;
615
616This simply dumps the HTML code for a given form element, followed by a
617C<span> that can contain validation error message.
618
619
620=head2 Enable the New Element Container When Building the Form
621
622Open C<lib/MyApp/Controller/Books.pm> in your editor. First add a
623C<use> for your element container class:
624
625 use FormElementContainer;
626
fb39c27a 627B<Note:> If you forget to C<use> your container class in your
628controller, then your form will not be displayed and no error messages
629will be generated. Don't forget this important step!
630
785c4199 631Then tell C<HTML::Widget> to use that class during rendering by updating
632C<make_book_widget> to match the following:
633
634 sub make_book_widget {
635 my ($self, $c) = @_;
636
637 # Create an HTML::Widget to build the form
638 my $w = $c->widget('book_form')->method('post');
639
640 # ***New: Use custom class to render each element in the form
641 $w->element_container_class('FormElementContainer');
642
643 # Get authors
644 my @authorObjs = $c->model("MyAppDB::Author")->all();
645 my @authors = map {$_->id => $_->last_name }
646 sort {$a->last_name cmp $b->last_name} @authorObjs;
647
648 # Create the form feilds
649 $w->element('Textfield', 'title' )->label('Title')->size(60);
650 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
651 # Convert to multi-select list
652 $w->element('Select', 'authors')->label('Authors')
653 ->options(@authors)->multiple(1)->size(3);
654 $w->element('Submit', 'submit' )->value('submit');
655
656 # Set constraints
657 $w->constraint(All => qw/title rating authors/)
658 ->message('Required. ');
659 $w->constraint(Integer => qw/rating/)
660 ->message('Must be an integer. ');
661 $w->constraint(Range => qw/rating/)->min(1)->max(5)
662 ->message('Must be a number between 1 and 5. ');
663 $w->constraint(Length => qw/title/)->min(5)->max(50)
664 ->message('Must be between 5 and 50 characters. ');
665
666 # Set filters
667 for my $column (qw/title rating authors/) {
668 $w->filter( HTMLEscape => $column );
669 $w->filter( TrimEdges => $column );
670 }
671
672 # Return the widget
673 return $w;
674 }
675
676The two new lines are marked with C<***New:>.
677
678
679=head2 Update the TT Template
680
681Open C<root/src/books/hw_form.tt2> and edit it to match:
682
683 [% META title = 'Create/Update Book' %]
684
685 [%# Comment out the auto-rendered form %]
686 [%# widget_result.as_xml %]
687
688
689 [%# Iterate over the form elements and display each -%]
690 <form name="book_form" action="[% widget_result.action %]" method="post">
691 <table border="0">
692 [% FOREACH element = widget_result.elements %]
693 <tr>
694 <td class="form-label">
695 [% element.label.as_text %]
696 </td>
697 <td class="form-element">
698 [% element.element_xml %]
699 <span class="form-error">
700 [% element.error_xml %]
701 </span>
702 </td>
703 </tr>
704 [% END %]
705 </table>
706 </form>
707
708
709 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
710
711
712 [%# A little JavaScript to move the cursor to the first field %]
713 <script LANGUAGE="JavaScript">
714 document.book_form.book_form_title.focus();
715 </script>
716
717This represents three changes:
718
719=over 4
720
721=item *
722
723The existing C<widget_result.as_xml> has been commented out.
724
725=item *
726
727It loops through each form element, displaying the field name in the
728first table cell along with the form element and validation errors in
729the second field.
730
731=item *
732
7d310f12 733JavaScript to position the user's cursor in the first field of the form.
785c4199 734
735=back
736
737
738=head2 Try Out the Form
739
740Press C<Ctrl-C> to kill the previous server instance (if it's still
741running) and restart it:
742
743 $ script/myapp_server.pl
744
745Try adding a book that validate. Return to the book list and the book
746you added should be visible.
747
748
4d583dd8 749=head1 AUTHOR
750
751Kennedy Clark, C<hkclark@gmail.com>
752
be16bacd 753Please report any errors, issues or suggestions to the author. The
7d310f12 754most recent version of the Catalyst Tutorial can be found at
be16bacd 755L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Runtime/lib/Catalyst/Manual/Tutorial/>.
4d583dd8 756
cc548726 757Copyright 2006, Kennedy Clark, under Creative Commons License
758(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).