Sunday, February 21, 2010

creating a FormHandler form from a DBIx::Class result source

Someone regularly pops up in #formhandler looking for a way to automatically create a form from a DBIx::Class result source. After explaining how to create a fairly simple role that would do that, the people have always gone away and nothing ever came of it.

I have mixed feelings about the idea, myself. Sure, there's a theoretical benefit to not defining things in multiple places. But in practice, there's almost always differences in selection, presentation, etc, that mean that an automatically derived form is of surprisingly little use. At least in my experience. YMMV. It's awfully easy to take the list of column names and type "has_field 'column';"

But then the eleventeenth person asked about automatic form generation - and the yaks must have been particularly hairy this weekend...because I made a good start at shaving this particular one.

There is now a new trait: HTML::FormHandler::TraitFor::DBICFields, and a package to do the mapping from source info to field definitions: HTML::FormHandler::Model::DBIC::TypeMap. I'm pretty sure that it's not quite right yet, but at least a start has been made, and it should be easier for those that want this kind of functionality to polish, add, and tweak. For one thing, it doesn't handle relationships yet. FormHandler can create form fields out of a lot of the standard DBIx::Class relationships, yet it is often not what you would want to have happen automatically. So I think skipping relationships as a default is probably OK.

Anyway, here is how to create a form by looking at the source columns:

   my $book = $schema->resultset('Book')->find(1);
   my $form = HTML::FormHandler::Model::DBIC->new_with_traits( 
      traits => ['HTML::FormHandler::TraitFor::DBICFields'],
      field_list => [ 'submit' => 
          { type => 'Submit', value => 'Save', order => 99 } ],
      item => $book );

The field_list attribute is because I thought it was better to explicitly specify the submit field, rather than automatically create it. I could be argued out of it...

The interface is only a few hours old, so if you have thoughts about how this should work, this is the time to speak up, before somebody starts using it and it becomes harder to change. :-)

Speaking of dynamic forms... HTML::FormHandler is known for it's Moose-y sugar interface, but there are other ways to create a FormHandler form. Using a 'field_list' as a parameter to 'new' works fine to create a dynamic form (though there are things that are possible inside a class that just can't be done with that kind of interface):

my @select_options = ( {value => 1, label => 'One'}, 
       {value => 2, label => 'Two'}, {value => 3, label => 'Three'} );
my $args =  {
    name       => 'test',
    field_list => [
        'username' => {
            type  => 'Text',
            apply => [ { check => qr/^[0-9a-z]*/, message => 
                   'Contains invalid characters' } ],
        },
        'password' => {
            type => 'Password',
        },
        'a_number' => {
            type      => 'IntRange',
            range_min => 12,
            range_max => 291,
        },
        'a_select' => {
            type    => 'Select',
            options => \@select_options,
        },
        'sub' => {
            type => 'Compound',
        },
        'sub.user' => {
            type  => 'Text',
            apply => [ { check => qr/^[0-9a-z]*/, 
               message => 'Not a valid user' } ],
        },
        'sub.name' => {
            type  => 'Text',
            apply => [ { check => qr/^[0-9a-z]*/, 
                message => 'Not a valid name' } ],
        },
        'submit' => {
            type => 'Submit',
        },
    ]
};
my $form = HTML::FormHandler->new( %$args );

And of course you can use one of the rendering roles, HTML::FormHandler::Render::Simple, or the rendering widgets, or write your own renderer... Too Many Ways To Do Things (tm).