One Definition, One Place: Form::Sensible::Reflector::DBIC and Catalyst

You've seen the form building frameworks. You've been there, done that, and hated it. But what irks you most is that you have to define your forms in multiple places, when you have a data source that has PLENTY of information to give you a quick, albeit simple, form. Well, I share your frustration, and although I can't say it makes me like forms any more, Form::Sensible::Reflector::DBIC was written by yours truly to ease a LOT of the pain associated with creating forms.

Where Do I use this Magical Beast?

Well, most of the time, I write my forms in standard, boring old HTML. This works great, and I can use Data::Manager for my data validation quite easily, but what if I have a project I want done yesterday, that has GIGANTIC forms that I don't feel like going through and creating by hand? This is a perfect scenario for Form::Sensible::Reflector::DBIC. In short, if you want rapid prototyping that can actually stick around and be extensible for a while, this module's for you.

What you Need

A Catalyst Application

That's what you're here for, right? :-)

DBIx::Class

My favorite ORM, but Form::Sensible::Reflector provides a base reflector class you could write a reflector for just about anything. This is what will be used specifically, as it gave me the best options data structure-wise from database<->forms.

Form::Sensible::Reflector::DBIC

This will be doing most of the work, at least generating the form.

Getting Started

Okay. So we have a scenario where we have an existing database, an existing DBIx::Class schema, but we need a quick interface we can set up and customize very, very quickly.

Let's start with our database schema (for convenience sake, we'll just use one table as an example):

    package My::Form::Sensible::App::Schema::Result::Entry;

    use base qw/DBIx::Class::Core/;

    __PACKAGE__->load_components(qw/ InflateColumn::DateTime TimeStamp  /);
    __PACKAGE__->table('entries');

    __PACKAGE__->add_columns(
    entryid => {
        data_type         => 'integer',
        is_nullable       => 0,
        is_auto_increment => 1,
    },
    title => {
        data_type   => 'varchar',
        size        => 150,
        is_nullable => 0,
    },
    body => {
        data_type   => "text",
        is_nullable => 0,
    },
    author => {
        data_type    => 'integer',
        is_nullable  => 0,
        render_hints => { field_type => "hidden" },
    },
    published => {
        data_type    => 'bool',
        is_nullable  => 0,
        default      => 0,
    },
    created_at => {
        data_type     => 'datetime',
        is_nullable   => 1,
        set_on_create => 1,
    },
    updated_at => {
        data_type     => 'datetime',
        is_nullable   => 1,
        set_on_create => 1,
        set_on_update => 1,
    },
    type => {
        data_type   => 'enum',
        is_nullable => 0,
        is_enum     => 1,
        extra       => { list => [qw/post page/] },
    },
    parent => {
        data_type   => 'int',
        is_nullable => 1,
        default     => 0,
    },
    path => {
        data_type   => 'varchar',
        size        => '100',
        is_nullable => 1,
        default     => '1',
    },

 );

  __PACKAGE__->add_unique_constraint( [qw/ title /] );

  __PACKAGE__->set_primary_key('entryid');

  1;

This is a typical, no frills DBIC schema representation of a table. We need to add a few things to tell FS::Reflector::DBIC how to render things:

   package  My::Form::Sensible::App::Schema::Result::Entry;

   use base qw/DBIx::Class::Core/;

   __PACKAGE__->load_components(qw/ InflateColumn::DateTime TimeStamp  /);
   __PACKAGE__->table('entries');

   __PACKAGE__->add_columns(
    entryid => {
        data_type         => 'integer',
        is_nullable       => 0,
        is_auto_increment => 1,
    },
    title => {
        data_type   => 'varchar',
        size        => 150,
        is_nullable => 0,
    },
    display_title => {
        data_type   => "varchar",
        size        => 250,
        is_nullable => 0,
        render_hints => { field_type => "hidden" },
    },
    body => {
        data_type   => "text",
        is_nullable => 0,
    },
    author => {
        data_type    => 'integer',
        is_nullable  => 0,
        render_hints => { field_type => "hidden" },
    },
    published => {
        data_type    => 'bool',
        is_nullable  => 0,
        default      => 0,
        render_hints => { on_label => "yes", off_label => "no", on_value => 1, off_value => 0 },
    },
    created_at => {
        data_type     => 'datetime',
        is_nullable   => 1,
        set_on_create => 1,
        render_hints  => { field_type => "hidden" },
    },
    updated_at => {
        data_type     => 'datetime',
        is_nullable   => 1,
        set_on_create => 1,
        set_on_update => 1,
        render_hints  => { field_type => "hidden" },
    },
    type => {
        data_type   => 'enum',
        is_nullable => 0,
        is_enum     => 1,
        extra       => { list => [qw/post page/] },
        render_hints =>
          { options => [ { name => 'page', value => 'page' }, { name => 'post', value => 'post' } ] }

    },
    parent => {
        data_type   => 'int',
        is_nullable => 1,
        default     => 0,
        render_hints => { field_type => "hidden" },
    },
    path => {
        data_type   => 'varchar',
        size        => '100',
        is_nullable => 1,
        default     => '1',
        render_hints => { field_type => "hidden" },
    },

 );

 __PACKAGE__->add_unique_constraint( [qw/ title /] );

 __PACKAGE__->set_primary_key('entryid');

 1;

So, basically, if we don't want something showing up in our form, we mark it as hidden. With things like an "enum", it's going to show up as a <select> drop down, and we want to make sure we get the fields named correctly.

So now, let's get a Catalyst controller up and going with our final reflector code:

    ## this could be any action in your app, this exact one is just a for instance:
    sub create : Chained('auth_base') PathPart('entry/new')  Args(0) {
        my ( $self, $c ) = @_;

        my $reflector = Form::Sensible::Reflector::DBIC->new;

        my $form =
          $reflector->reflect_from( $c->model('Database::Entry'), { form => { name => 'entry' } } );
        my $renderer = Form::Sensible->get_renderer('HTML');
        $c->stash( form => $renderer->render($form), form_object => $form, post_to => '/entry/new' );    # hate this

    }




The template to display this:

    [% form.complete( post_to, 'POST') %]

And that's really it. Basically, I've tried to keep the SQL data types relatively intuitive. If a column is supposed to take a textfield's worth of data, it's probably a textfield. If it's got options, you get to decide what it should be. At this point, things like enum are <select> fields. Form::Sensible is very flexible in allowing you to specify what you want to look like what, so post-creation, you can even munge the fields to look like what you want.

We get a form like this:

You'll notice that I've used TinyMCE for the main textarea. What's great about Form::Sensible is it creates sensible divs for your CSS to be styled, and since TinyMCE just uses the DOM, you just point it at whatever id or element you want styled as a WYSIWYG editor.

That's really it folks. Form::Sensible::Reflector::DBIC only does a few things:

* Set up a bridge between Form::Sensible and DBIx::Class
* Allow you to have form definitions in one place, as opposed to two or three, like other systems might have you do.
* Quickly display your form without much fuss.

Define your tables, tinker, display. That's all. I hate forms, let's make it painless :-)

AUTHOR

Devin Austin <dhoss@cpan.org>