[Cross-posted to catalyst-users and rhtmlo lists]

I know there are several modules out there that hook up rhtmlo and catalyst, but none of them do what I want. They all seem to do too much: connect to a CRUD API, interface with rdbo, build a form object from a config file, etc. I really just need a simple glue to load rhtmlo-derived form classes into my catalyst controller actions and initialize them with any query params, so I created a base controller and an action class that take care of the details. It works for me, but I thought it might be useful to the larger community, so I made it configurable and documented it pretty thoroughly. But before I pollute CPAN with one more piece of cruft, I want to be sure it's sensible/useful. The two packages are defined below. Comments would be appreciated.

package Catalyst::Controller::RHTMLO;

use strict;
use warnings;

use base 'Catalyst::Controller';
use MRO::Compat; # to get $self->next::method() right

=head1 NAME

Catalyst::Controller::RHTMLO - Catalyst Base Controller for Rose::HTML::Objects forms

=head1 SYNOPSIS

    package MyApp::Controller::Books;
    use base 'Catalyst::Controller::RHTMLO';

    # loads MyApp::Form::Book (which 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 two search forms on same page
    sub search : Local Form('ByAuthor,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 Catalyst actions to form classes derived from
L<Rose::HTML::Form>, a component of John Siracusa's excellent L<Rose::Object> 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 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 display more than one distinct form on a page, just list them all in the
attribute, delimited with commas or spaces:

    sub search : Local Form('ByAuthor,ByTitle,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');
    }

(Note that you must put all the form names inside one set of quotes; I<DO NOT>
try to quote each individual form name. This is a limitation of perl5's
subroutine attributes.) The first form listed will still be stored in the stash in the usual location; I<all> the forms (including the first) will be stored
under a separate stash key, in a hash keyed to the name used to load them.

If for some reason you need to render the same form more than once on a page,
just list it again:

    sub search : Local Form('Search,Search') {
        my ($self, $c) = @_;

        $c->stash->{forms}{Search}[0]->name('search_0');
        $c->stash->{forms}{Search}[1]->name('search_1');
    }

In this (weird but possible) case, the forms will be put into an arrayref in the expected location. I haven't actually attempted to use this technique in production, so I don't know what else you might have to do to make it work...

=head1 CONFIGURATION

You can override many defaults using Catalyst's configuration mechanism:

    __PACKAGE__->config(
        'Controller::RHTMLO' => {
            form_attr       => 'HasForm',
            action_class    => 'MyApp::Action::RoseForm',
            stash_name      => 'formobj',
            stash_hash_name => 'allforms',
            form_prefix     => 'MyApp::RoseForm',
        }
    );

=over

=item C<action_class> (default 'Catalyst::Controller::RHTMLO::Action')

If you want to add more functionality to the automatic form loading and initialization, you can create your own 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: '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: 'MyApp::Form' (using your app's actual name). Set this to the
namespace where your Rose::HTML::Form subclasses live.

=item C<stash_hash_name>

Default: 'forms'. Sets the stash key under which all forms for a given action
will be stored by class name.

=item C<stash_name>

Default: 'form'. Sets the stash key under which the first 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. Based on L<CGI::FormBuilder> rather than
Rose::HTML::Form.

=item L<CatalystX::RoseIntegrator>

Looks like it uses a CGI::FormBuilder-style config file to construct
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 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::Object>,
L<Catalyst::Controller>, L<Catalyst::Action>, L<Catalyst>

=head1 AUTHOR

Jason Gottshall <jgottshall att capwiz dott com>

=head1 LICENSE

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut

sub create_action {
    my ($self, %args) = @_;

    my $config = $self->config->{'Controller::RHTMLO'};
    my $form_attr = $config->{'form_attr'} || 'Form';

    if( exists $args{attributes}{$form_attr} ) {
my $action_class = $config->{'action_class'} || 'Catalyst::Controller::RHTMLO::Action';
        push @{ $args{attributes}{ActionClass} }, $action_class;

        if(my $val = delete $args{attributes}{$form_attr}) {
            $args{_form_class} = $val;
        }
    }

    return $self->next::method(%args);
}

package Catalyst::Controller::RHTMLO::Action;

use strict;
use warnings;

use base 'Catalyst::Action';
use MRO::Compat;
use Catalyst::Utils;

__PACKAGE__->mk_accessors(qw/_form_class/);

sub execute {
    my $self = shift;
    my ($controller, $c, @args) = @_;

    $self->get_forms($c);

    return $self->next::method(@_);
}

sub get_forms {
    my ($self, $c) = @_;

    # sanity check; ensure we actually declared form class
    return unless $self->_form_class && ref $self->_form_class eq 'ARRAY';

    # form classes are delimited by spaces or commas
    my @classes = split /[ ,]+/, $self->_form_class->[0];
    unless(@classes) {
$c->log->warn('No form class specified for action "' . $self->reverse . '"');
        return;
    }

    my $config      = $c->config->{'Controller::RHTMLO'};
    my $stash_name  = $config->{'stash_name'}      || 'form';
    my $stash_hash  = $config->{'stash_hash_name'} || 'forms';
my $form_prefix = $config->{'form_prefix'} || $c->config->{'name'} . '::Form';
    $form_prefix .= '::';

    foreach my $name (@classes) {
        next unless $name; # ignore leading/trailing delimiters

        my $class = $name;
        # allow for full class names with leading '+'
        $class = $form_prefix . $class unless $class =~ s/^\+//;
        Catalyst::Utils::ensure_class_loaded($class);
        $c->log->debug("Loading form '$class'");

        # setup form
        my $form = $class->new();
        $form->params($c->req->params);
        $form->init_fields;

        # put form in stash under its name
        if(my $prev_form = $c->stash->{$stash_hash}->{$name}) {
            # multiple instances of same form class are stored in arrayref
            $c->stash->{$stash_hash}->{$name} = [$prev_form]
                unless ref $prev_form eq 'ARRAY';

            push @{$c->stash->{$stash_hash}->{$name}}, $form;
        }
        else {
            $c->stash->{$stash_hash}->{$name} = $form;
        }

        # create shortcut to "main" form
        $c->stash->{$stash_name} ||= $form;
    }

    return;
}

1;



_______________________________________________
List: [email protected]
Listinfo: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/catalyst
Searchable archive: http://www.mail-archive.com/[email protected]/
Dev site: http://dev.catalyst.perl.org/

Reply via email to