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