On Sat, Jan 31, 2009 at 12:03 AM, Jason Gottshall <jgottsh...@capwiz.com> wrote: > Ok, for those of you who have been waiting with bated breath, here's my > second attempt at a base controller for loading Rose::HTML::Form classes. > I've completely refactored/rewritten the configuration stuff after a long > chat with mst. I also eliminated the handling of multiple instances of the > same form in the same action, having determined that it's just not a > meaningful case. I did *not* implement any caching mechanism, based on my > concern about form objects being modified beyond simple parameter > initialization. > > I think this version is a *lot* better, and I'd appreciate any additional > feedback before I package it up for CPAN. Thanks! > > package Catalyst::Controller::RHTMLO; > > use strict; > use warnings; > > use base 'Catalyst::Controller'; > use MRO::Compat; # to get $self->next::method() right > use Catalyst::Utils; > > =head1 NAME > > Catalyst::Controller::RHTMLO - Catalyst base controller for integrating > Rose::HTML::Form objects > > =head1 SYNOPSIS > > package MyApp::Controller::Books; > use base 'Catalyst::Controller::RHTMLO'; > > # loads MyApp::Form::Book, which should ->isa('Rose::HTML::Form') > sub edit : Local Form('Book') { > my ( $self, $c ) = @_; > > # form object is already init'ed with params and stashed > my $form = $c->stash->{form}; > > if ( $form->was_submitted ) { > if ( $form->validate ) { > # write to db or whatever > } > else { > # show errors or whatever > } > } > } > > # display several search forms on same page > sub search : Local Form('ByAuthor') Form('ByTitle') { > my ( $self, $c ) = @_; > > if ( $c->stash->{forms}->{ByAuthor}->was_submitted ) { > # look up books by author > } > elsif { $c->stash->{forms}->{ByTitle}->was_submitted ) { > # look up books by title, duh > } > } > > =head1 DESCRIPTION > > This base controller glues L<Catalyst> actions to form classes derived from > L<Rose::HTML::Form>, a component of John Siracusa's burgeoning L<Rose> > framework. Unlike some other form-loading modules (see L</"PRIOR ART">), > this one does not include any mechanism for defining form structures; it > merely loads, instantiates, and initializes pre-written form classes for use > in your controllers. > > In order to utilize a particular form in a particular Catalyst action, > simply declare an attribute on the subroutine: > > sub edit : Local Form('Book') { } > > This will ensure that C<MyApp::Form::Book> is loaded and initialized, > basically equivalent to the following: > > my $form = MyApp::Form::Book->new(); > $form->params($c->req->params); > $form->init_fields; > $c->stash->{form} = $form; > > The namespace used to complete the form class name is > L<configurable|/CONFIGURATION>, or you can specify a full package name by > prepending a 'plus' sign: > > sub edit : Local Form('+My::FormClasses::Book') { } > > To utilize more than one distinct form class in the same action, simply > declare additional attributes: > > sub search : Local Form('ByAuthor') Form('ByTitle') Form('BySubject') { > my ($self, $c) = @_; > > > $c->stash->{forms}{ByAuthor}->action($c->uri_for('/search/byauthor')); > $c->stash->{forms}{ByTitle}->method('GET'); > $c->stash->{forms}{BySubject}->name('bytopic'); > } > > The first form listed will be stored in the stash in the usual location; > I<all> the forms (including the first) will be stored under a separate > (L<configurable|/CONFIGURATION>) stash key, in a hash keyed to the name used > to load them. > > =head1 CONFIGURATION > > You can override many defaults using Catalyst's configuration mechanism: > > __PACKAGE__->config( > # settings for all controllers using this base > 'Catalyst::Controller::RHTMLO' => { > form_attr => 'HasForm', > form_action_class => 'MyApp::Action::RoseForm', > form_stash_name => 'formobj', > form_stash_hash => 'allforms', > form_prefix => 'MyApp::RoseForm', > }, > # settings for specific controllers in MyApp > 'Controller::Foo' => { # or 'C::Foo' if MyApp is built that way > form_prefix => 'MyApp::FooForms', > }, > ); > > =over > > =item C<form_action_class> > > Default: C<'Catalyst::Controller::RHTMLO::Action'> > > If you want to add more functionality to the automatic form loading and > initialization, you can create your own custom action class: > > package MyApp::Action::RoseForm; > use base 'Catalyst::Controller::RHTMLO::Action'; > > sub execute { > my $self = shift; > my ($controller, $c, @args) = @_; > > # load forms via base class > $self->next::method(@_); > > # do cool stuff > $c->stash->{form}->add_fields( > secure_token => { > type => 'hidden', > value => $c->some_cool_security_token > } > ); > return; > } > > =item C<form_attr> > > Default: C<'Form'> > > Set this to alter the subroutine attribute used to indicate one or more > forms to be loaded by a given action, e.g.: > > sub edit : Local HasForm('Books') { } > > =item C<form_prefix> > > Default: C<'MyApp::Form'> (using your app's actual name) > > Set this to the namespace where your Rose::HTML::Form subclasses live. > > =item C<stash_hash> > > Default: C<'forms'> > > Sets the stash key under which all forms for a given action will be stored, > keyed according to the name used to load them. > > =item C<stash_name> > > Default: C<'form'> > > Sets the stash key under which the first (and often I<only>) form for a > given action will be stored. > > =head1 PRIOR ART > > There are several other modules on CPAN that do similar things, many having > inspired this module in various ways. > > =over > > =item L<Catalyst::Controller::FormBuilder> > > Provided a lot of insight into how to trigger the form loading process with > a custom subroutine attribute and custom action class. Based on > L<CGI::FormBuilder> (rather than L<Rose::HTML::Form>). > > =item L<CatalystX::RoseIntegrator> > > Looks like it uses a L<CGI::FormBuilder>-style config file to construct > L<Rose::HTML::Form> objects on the fly, rather than having static > subclasses. Also seems to include direct model integration with > L<Rose::DB::Object>. > > =item L<CatalystX::CRUD::Controller::RHTMLO> > > A component that enables use of L<Rose::HTML::Form> objects with Peter > Karman's cool L<CatalystX::CRUD> API. > > =back > > =head1 SEE ALSO > > L<Rose::HTML::Form>, L<Rose::HTML::Objects>, L<Rose>, > L<Catalyst::Controller>, L<Catalyst::Action>, L<Catalyst> > > =head1 AUTHOR > > Jason Gottshall <jgottshall att capwiz dott com> > > =head1 ACKNOWLEDGEMENTS > > mst: for patiently helping me make sense of Catalyst's configuration system > phaylon: for pointing me to existing documentation on extending components > > =head1 LICENSE > > This program is free software; you can redistribute it and/or modify it > under the same terms as Perl itself. > > =cut > > __PACKAGE__->mk_accessors( > qw/ > form_attr > form_action_class > form_stash_name > form_stash_hash > form_prefix > / > ); > > __PACKAGE__->config( > form_attr => 'Form', > form_action_class => 'Catalyst::Controller::RHTMLO::Action', > form_stash_name => 'form', > form_stash_hash => 'forms', > ); > > sub COMPONENT { > my ( $self, $c, $arguments ) = @_; > > # merge global config with controller-specific config > #TODO refactor this into Moose role for Catalyst components > $arguments = $self->merge_config_hashes( > $c->config->{'Catalyst::Controller::RHTMLO'}, > $arguments > ); > > # set default form prefix according to appname > $self->config->{form_prefix} = $c->config->{name} . "::Form"; > > return $self->next::method( $c, $arguments ); > } > > sub create_action { > my ( $self, %args ) = @_; > > if ( my $formnames = delete $args{attributes}{ $self->form_attr } ) > { > # ensure at least one form name is provided > @$formnames = grep {$_} @$formnames; > unless (@$formnames) { > die sprintf > q{Attribute '%s()' for action '/%s' must specify an > RHTMLO-based form class}, > $self->form_attr, $args{reverse}; > } > > # validate and load form classes > foreach my $name (@$formnames) { > my $class = > $name =~ s/^\+// > ? $name # leading plus indicates fully-qualified package > : join( '::', $self->form_prefix, $name ); > Catalyst::Utils::ensure_class_loaded($class); > $args{form_classes}{$name} = $class; > } > > # pass config to action > $args{stash_name} = $self->form_stash_name; > $args{stash_hash} = $self->form_stash_hash; > > # set action class > push @{ $args{attributes}{ActionClass} }, $self->form_action_class; > } > > return $self->next::method(%args); > } > > 1; > package Catalyst::Controller::RHTMLO::Action; > > use strict; > use warnings; > > use base 'Catalyst::Action'; > use MRO::Compat; > use Catalyst::Utils; > > __PACKAGE__->mk_accessors(qw/stash_name stash_hash form_classes/); > > sub execute { > my $self = shift; > my ( $controller, $c, @args ) = @_; > > $self->_setup_forms($c); > > return $self->next::method(@_); > } > > sub _setup_forms { > my ( $self, $c ) = @_; > > while ( my ( $name, $class ) = each %{ $self->form_classes } ) { > # setup form > $c->log->debug("Loading form '$class' as '$name'"); > my $form = $class->new(); > $form->params( $c->req->params ); > $form->init_fields; > > # put form in stash (first or only) > $c->stash->{ $self->stash_name } ||= $form; > > # put form in stash under its name, in case of multiple forms > $c->stash->{ $self->stash_hash }->{$name} = $form; > } >
Maybe I am just being lazy now - but before I start digging too deep - did you take into accout that some forms require loading stuff from the DB? It can happen in to cases: 1) loading SELECT choices lists - this one is easy - because it can be done at initialisation as it does not depend on th request. 2) loading related forms for one to many relations - where you don't know how many related forms you need until you have the main object - i.e. until request time. See Rose::HTML::Form::Repeatable. Cheers, Zbigniew http://brudnopis.blogspot.com/ http://perlalchemy.blogspot.com/ _______________________________________________ List: Catalyst@lists.scsys.co.uk Listinfo: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/catalyst Searchable archive: http://www.mail-archive.com/catalyst@lists.scsys.co.uk/ Dev site: http://dev.catalyst.perl.org/