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