added recipe for content disposition from dwc
[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
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
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';
7d310f12 547
548 # Redisplay an empty form for another
549 $c->stash->{widget_result} = $w->result;
4d583dd8 550 }
551
552 # Set the template
553 $c->stash->{template} = 'books/hw_form.tt2';
554 }
555
64ccd8a8 556In this version of C<hw_create_do> we removed the logic that manually
557pulled the form variables and used them to call
558C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
559single call to C<$book-E<gt>populate_from_widget>. Note that we still
560have to call C<$book-E<gt>add_to_book_authors> once per author because
561C<populate_from_widget> does not currently handle the relationships
7d310f12 562between tables. Also, we reset the form to an empty fields by adding
563another call to C<$w-E<gt>result> and storing the output in the stash
564(if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
565the form values already entered will be retained on redisplay --
566although this could be desirable for some applications, we avoid it
567here to help avoid the creation of duplicate records).
4d583dd8 568
cc548726 569
785c4199 570=head2 Try Out the Form
571
572Press C<Ctrl-C> to kill the previous server instance (if it's still
573running) and restart it:
574
575 $ script/myapp_server.pl
576
577Try adding a book that validate. Return to the book list and the book
578you added should be visible.
579
580
581
582=head1 Rendering C<HTMLWidget> Forms in a Table
583
584Some developers my wish to use the "old-fashioned" table style of
585rendering a form in lieu of the default C<HTML::Widget> rendering that
7d310f12 586assumes you will use CSS for formatting. This section demonstrates
587some techniques that can override the default rendering with a
588custom class.
785c4199 589
590
591=head2 Add a New "Element Container"
592
593Open C<lib/FormElementContainer.pm> in your editor and enter:
594
595 package FormElementContainer;
596
597 use base 'HTML::Widget::Container';
598
599 sub _build_element {
600 my ($self, $element) = @_;
601
602 return () unless $element;
603 if (ref $element eq 'ARRAY') {
604 return map { $self->_build_element($_) } @{$element};
605 }
606 my $e = $element->clone;
607 my $class = $e->attr('class') || '';
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/>).