Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8: Advanced CRUD
-
=head1 OVERVIEW
This is B<Part 8 of 9> for the Catalyst tutorial.
=item 9
-L<Appendicies|Catalyst::Manual::Tutorial::Appendicies>
+L<Appendices|Catalyst::Manual::Tutorial::Appendices>
=back
-
-
=head1 DESCRIPTION
This part of the tutorial explores more advanced functionality for
forms and model objects.
In keeping with the Catalyst (and Perl) spirit of flexibility, there are
-many different ways approach advanced CRUD operations in a Catalyst
+many different ways to approach advanced CRUD operations in a Catalyst
environment. One alternative is to use
-L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold>
-to instantly construct a set of Controller methods and templates for
-basic CRUD operations. Although a popular subject in Quicktime movies
-that serve as promotional material for various frameworks, more
-real-world applications require more control. Other options include
-L<Data::FormValidator|Data::FormValidator> and
+L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold>
+to instantly construct a set of Controller methods and templates for
+basic CRUD operations. Although a popular subject in Quicktime
+movies that serve as promotional material for various frameworks,
+real-world applications generally require more control. Other
+options include L<Data::FormValidator|Data::FormValidator> and
L<HTML::FillInForm|HTML::FillInForm>.
-Here, we will make use of the L<HTML::Widget|HTML::Widget> to not only
-ease form creation, but to also provide validation of the submitted
-data. The approached used by the part of the tutorial is to slowly
-incorporate additional L<HTML::Widget|HTML::Widget> functionality in a
-step-wise fashion (we start with fairly simple form creation and then
-move on to more complex and "magical" features such as validation and
+Here, we will make use of the L<HTML::Widget|HTML::Widget> to not only
+ease form creation, but to also provide validation of the submitted
+data. The approached used by this part of the tutorial is to slowly
+incorporate additional L<HTML::Widget|HTML::Widget> functionality in a
+step-wise fashion (we start with fairly simple form creation and then
+move on to more complex and "magical" features such as validation and
auto-population/auto-saving).
-B<Note:> Part 8 of the tutorial is optional. Users who do not which to
-use L<HTML::Widget|HTML::Widget> may skip this section.
-
-B<TIP>: Note that all of the code for this part of the tutorial can be
-pulled from the Catalyst Subversion repository in one step with the
-following command:
-
- svn checkout http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/Tutorial@###
- IMPORTANT: Does not work yet. Will be completed for final version.
-
+B<Note:> Part 8 of the tutorial is optional. Users who do not wish to
+use L<HTML::Widget|HTML::Widget> may skip this part.
+You can checkout the source code for this example from the catalyst subversion repository as per the instructions in L<Catalyst::Manual::Tutorial::Intro>
=head1 C<HTML::WIDGET> FORM CREATION
This section looks at how L<HTML::Widget|HTML::Widget> can be used to
add additional functionality to the manually created form from Part 3.
-
=head2 Add the C<HTML::Widget> Plugin
Open C<lib/MyApp.pm> in your editor and add the following to the list of
-plugins (be sure to leave the existing plugins enabled:
+plugins (be sure to leave the existing plugins enabled):
HTML::Widget
-
=head2 Add a Form Creation Helper Method
Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
sub make_book_widget {
my ($self, $c) = @_;
-
+
# Create an HTML::Widget to build the form
my $w = $c->widget('book_form')->method('post');
$w->element('Select', 'authors')->label('Authors')
->options(@authors);
$w->element('Submit', 'submit' )->value('submit');
-
+
# Return the widget
return $w;
}
-This method provides a central location (so it can be called by multiple
-actions, such as create and edit) that builds an HTML::Wiget-based form
-with the appropriate fields. The "Get Authors" code uses DBIC to
-retrieve a list of model objects and then uses C<map> to quickly create
-a hash where the hash keys are the database primary keys from the
-authors table and the associated values are the last names of the
+This method provides a central location that builds an
+HTML::Widget-based form with the appropriate fields. The "Get authors"
+code uses DBIC to retrieve a list of model objects and then uses C<map>
+to create a hash where the hash keys are the database primary keys from
+the authors table and the associated values are the last names of the
authors.
-
=head2 Add Actions to Display and Save the Form
Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
# Set a status message for the user
$c->stash->{status_msg} = 'Book created';
- # Use 'hw_create' to redisplay the form
+ # Use 'hw_create' to redisplay the form. As discussed in
+ # Part 3, 'detach' is like 'forward', but it does not return
$c->detach('hw_create');
-
}
Note how we use C<make_book_widget> to build the core parts of the form
(e.g., C<hw_create_do> for a create operation but C<hw_update_do> to
update an existing book object).
+B<NOTE:> If you receive an error about Catalyst not being able to find
+the template C<hw_create_do.tt2>, please verify that you followed the
+instructions in the final section of
+L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> where
+you returned to a manually specified template. You can either use
+C<forward>/C<detach> B<OR> default template names, but the two cannot
+be used together.
+
=head2 Update the CSS
messages in red. Note that we are pulling the color scheme settings
from the C<root/lib/config/col> file that was created by the TTSite
helper. This allows us to change the color used by various error styles
-in the CCS from a single location.
-
+in the CSS from a single location.
=head2 Create a Template Page To Display The Form
-C<root/src/books/hw_form.tt2>
+Open C<root/src/books/hw_form.tt2> in your editor and enter the following:
+
[% META title = 'Create/Update Book' %]
[% widget_result.as_xml %]
<p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
-
=head2 Add Links for Create and Update via C<HTML::Widget>
Open C<root/src/books/list.tt2> in your editor and add the following to
<p>
HTML::Widget:
<a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
- <a href="[% Catalyst.uri_for('hw_update') %]">Update</a>
</p>
drop-down list, there are no restrictions on items such as the length of
the title (for example, you can create a one-letter title) and value for
the rating (you can use any number you want, and even non-numeric values
-with SQLite). The next section seeks to address this concern.
+with SQLite). The next section will address this concern.
B<Note:> Depending on the database you are using and how you established
the columns in your tables, the database could obviously provide various
the previous paragraph is that the I<web application> itself is not
performing any validation.
-
=head1 C<HTML::WIDGET> VALIDATION AND FILTERING
Although the use of L<HTML::Widget|HTML::Widget> in the previous section
extraneous whitespace from fields or to escape meta-characters in user
input.
-
=head2 Add Constraints and Filters to the Widget Creation Method
Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
sub make_book_widget {
my ($self, $c) = @_;
-
+
# Create an HTML::Widget to build the form
my $w = $c->widget('book_form')->method('post');
my @authorObjs = $c->model("MyAppDB::Author")->all();
my @authors = map {$_->id => $_->last_name }
sort {$a->last_name cmp $b->last_name} @authorObjs;
-
+
# Create the form feilds
$w->element('Textfield', 'title' )->label('Title')->size(60);
$w->element('Textfield', 'rating' )->label('Rating')->size(1);
=back
-
=head2 Rebuild the Form Submission Method to Include Validation
Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
process the form in C<hw_create_do> I<and> forward/detach back to
<hw_create>, we would end up with C<make_book_widget> being called
twice, resulting in a duplicate set of elements being added to the form.
+(There are other ways to address the "duplicate form rendering" issue --
+just be aware that it exists.)
=item *
=back
-
=head2 Try Out the Form
-Press C<Ctrl-C> to kill the previous server instance (if it's still running) and restart it:
+Press C<Ctrl-C> to kill the previous server instance (if it's still
+running) and restart it:
$ script/myapp_server.pl
=item *
-fill_wiget()
+fill_widget()
Takes data from the database and transfers it to your form widget.
In other words, the two methods are a mirror image of each other: one
reads from the database while the other writes to the database.
-
=head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
__PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
-
=head2 Use C<populate_from_widget> in C<hw_create_do>
Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
}
# Set a status message for the user
- $c->stash->{status_msg} = 'Book created';
+ $c->flash->{status_msg} = 'Book created';
+
+ # Redisplay an empty form for another
+ $c->stash->{widget_result} = $w->result;
}
# Set the template
single call to C<$book-E<gt>populate_from_widget>. Note that we still
have to call C<$book-E<gt>add_to_book_authors> once per author because
C<populate_from_widget> does not currently handle the relationships
-between tables.
+between tables. Also, we reset the form to an empty fields by adding
+another call to C<$w-E<gt>result> and storing the output in the stash
+(if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
+the form values already entered will be retained on redisplay --
+although this could be desirable for some applications, we avoid it
+here to help avoid the creation of duplicate records).
+=head2 Try Out the Form
-=head1 AUTHOR
+Press C<Ctrl-C> to kill the previous server instance (if it's still
+running) and restart it:
-Kennedy Clark, C<hkclark@gmail.com>
+ $ script/myapp_server.pl
+
+Try adding a book that validates. Return to the book list and the book
+you added should be visible.
+
+
+
+=head1 Rendering C<HTMLWidget> Forms in a Table
+
+Some developers my wish to use the "old-fashioned" table style of
+rendering a form in lieu of the default C<HTML::Widget> rendering that
+assumes you will use CSS for formatting. This section demonstrates
+some techniques that can override the default rendering with a
+custom class.
+
+
+=head2 Add a New "Element Container"
-Please report any errors, issues or suggestions to the author.
+Open C<lib/FormElementContainer.pm> in your editor and enter:
+
+ package FormElementContainer;
+
+ use base 'HTML::Widget::Container';
+
+ sub _build_element {
+ my ($self, $element) = @_;
+
+ return () unless $element;
+ if (ref $element eq 'ARRAY') {
+ return map { $self->_build_element($_) } @{$element};
+ }
+ my $e = $element->clone;
+ $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
+ if $self->error && $e->tag eq 'input';
+
+ return $e ? ($e) : ();
+ }
+
+ 1;
-Copyright 2006, Kennedy Clark. All rights reserved.
+This simply dumps the HTML code for a given form element, followed by a
+C<span> that can contain validation error message.
-This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
-Version: .94
+=head2 Enable the New Element Container When Building the Form
+
+Open C<lib/MyApp/Controller/Books.pm> in your editor. First add a
+C<use> for your element container class:
+
+ use FormElementContainer;
+
+B<Note:> If you forget to C<use> your container class in your
+controller, then your form will not be displayed and no error messages
+will be generated. Don't forget this important step!
+
+Then tell C<HTML::Widget> to use that class during rendering by updating
+C<make_book_widget> to match the following:
+
+ sub make_book_widget {
+ my ($self, $c) = @_;
+
+ # Create an HTML::Widget to build the form
+ my $w = $c->widget('book_form')->method('post');
+
+ # ***New: Use custom class to render each element in the form
+ $w->element_container_class('FormElementContainer');
+
+ # Get authors
+ my @authorObjs = $c->model("MyAppDB::Author")->all();
+ my @authors = map {$_->id => $_->last_name }
+ sort {$a->last_name cmp $b->last_name} @authorObjs;
+
+ # Create the form feilds
+ $w->element('Textfield', 'title' )->label('Title')->size(60);
+ $w->element('Textfield', 'rating' )->label('Rating')->size(1);
+ # Convert to multi-select list
+ $w->element('Select', 'authors')->label('Authors')
+ ->options(@authors)->multiple(1)->size(3);
+ $w->element('Submit', 'submit' )->value('submit');
+
+ # Set constraints
+ $w->constraint(All => qw/title rating authors/)
+ ->message('Required. ');
+ $w->constraint(Integer => qw/rating/)
+ ->message('Must be an integer. ');
+ $w->constraint(Range => qw/rating/)->min(1)->max(5)
+ ->message('Must be a number between 1 and 5. ');
+ $w->constraint(Length => qw/title/)->min(5)->max(50)
+ ->message('Must be between 5 and 50 characters. ');
+
+ # Set filters
+ for my $column (qw/title rating authors/) {
+ $w->filter( HTMLEscape => $column );
+ $w->filter( TrimEdges => $column );
+ }
+
+ # Return the widget
+ return $w;
+ }
+
+The two new lines are marked with C<***New:>.
+
+
+=head2 Update the TT Template
+
+Open C<root/src/books/hw_form.tt2> and edit it to match:
+
+ [% META title = 'Create/Update Book' %]
+
+ [%# Comment out the auto-rendered form %]
+ [%# widget_result.as_xml %]
+
+
+ [%# Iterate over the form elements and display each -%]
+ <form name="book_form" action="[% widget_result.action %]" method="post">
+ <table border="0">
+ [% FOREACH element = widget_result.elements %]
+ <tr>
+ <td class="form-label">
+ [% element.label.as_text %]
+ </td>
+ <td class="form-element">
+ [% element.element_xml %]
+ <span class="form-error">
+ [% element.error_xml %]
+ </span>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </form>
+
+
+ <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
+
+
+ [%# A little JavaScript to move the cursor to the first field %]
+ <script LANGUAGE="JavaScript">
+ document.book_form.book_form_title.focus();
+ </script>
+
+This represents three changes:
+
+=over 4
+
+=item *
+
+The existing C<widget_result.as_xml> has been commented out.
+
+=item *
+
+It loops through each form element, displaying the field name in the
+first table cell along with the form element and validation errors in
+the second field.
+
+=item *
+
+JavaScript to position the user's cursor in the first field of the form.
+
+=back
+
+
+=head2 Try Out the Form
+
+Press C<Ctrl-C> to kill the previous server instance (if it's still
+running) and restart it:
+
+ $ script/myapp_server.pl
+
+Try adding a book that validates. Return to the book list and the book
+you added should be visible.
+
+
+=head1 AUTHOR
+
+Kennedy Clark, C<hkclark@gmail.com>
+
+Please report any errors, issues or suggestions to the author. The
+most recent version of the Catalyst Tutorial can be found at
+L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Runtime/lib/Catalyst/Manual/Tutorial/>.
+Copyright 2006, Kennedy Clark, under Creative Commons License
+(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).