I saw some article about cpan:HTML::FormHandler, and decided I'd give it a try. I'm probably not really using it as intended... and I'm also not pushing this confluence of tech nearly as far as it can go.
#!/usr/bin/perl
use strict;
use MooseX::Declare;
use KiokuDB;
use Continuity;
First we just pull in some libraries. We'll use cpan:MooseX::Declare for fun :) . Now let's make a lovely base class for our forms, to set up some good defaults an an updater.
# Base class for all forms
class Form
extends HTML::FormHandler {
has owner => (is => 'rw');
our $form_count = 0;
has '+name' => ( default => sub { return "f-" . $form_count++ } );
has '+html_prefix' => ( default => 1 );
method update {
if($self->validated) {
my $v = $self->value;
foreach my $field (keys %$v) {
eval { # ignore validation issues :)
$self->owner->$field($v->{$field})
if $self->owner->can($field);
}
}
}
}
}
That stuff about the form name is so we can display multiple forms on a page, each with it's own unique name. The update method ties my form to my actual object. I ignore errors for now... though really that's losing a big part of what cpan:HTML::FormHandler provides.
class BasicPersonForm
extends Form
with HTML::FormHandler::Render::Table {
has '+field_list' => ( default => sub {[
name => 'Text',
birthday => 'Text',
save => { type => 'Submit', value => 'Save' },
]});
}
Here I'm building on my Form base, making a form specific for my Person class. This is declaratively specifying which fields to show and in what order, along with a lovely save button. In theory I'll be able to expand this into subforms and compose them and so on.
Though I'm roughly OK with listing inputs for the form, it annoys me to have to specify the types. That, at at least a basic level, should be readable from the fields to which this form is connected. So one thing I'm noodling is using the rendering and validation, but importing the types from the data class. I think the cpan:Form::Processor::Model::DBIC does this a bit.
class Person {
use MooseX::Types::DateTimeX qw( DateTime );
has name => ( is => 'rw', default => sub { '' } );
has birthday => ( is => 'rw', isa => DateTime, coerce => 1 );
method basic_form { BasicPersonForm->new( owner => $self, init_object => $self ) }
}
My Person class is straightforward. I used the DateTimeX thingie to auto coerce stuff magically. And that basic_form method is just a helper to create a form for a given instance.
sub main {
my $r = shift;
my $db = KiokuDB->connect(
'dbi:SQLite:phone.db',
create => 1
);
my $scope = $db->new_scope;
# This is a new person, pending some data
my $new_person = Person->new;
while(1) {
my @people = $db->grep(sub { ref($_) eq 'Person' })->all;
my @forms = map { $_->basic_form } @people;
foreach my $form (@forms) {
$r->print($form->render);
}
$r->print("<h3>Add person:</h3>");
my $new_person_form = $new_person->basic_form;
$r->print($new_person_form->render);
$r->next;
my $params = { $r->params };
foreach my $form (@forms) {
$form->process( $params );
$form->update;
$db->update( $form->owner );
}
$new_person_form->process( $params );
$new_person_form->update;
if($new_person->name) {
$db->insert( $new_person );
# And now queue up another new one
$new_person = Person->new;
}
}
}
This is a little Continuity app, so main gets called and passed $r, which is the request handle. After setting up our KiokuDB (with SQLite backend), I enter an endless loop of showing the forms and the processing the results. Oh, and I create a special Person instance for holding a new person.
In the loop, I grab all the people objects in the DB at once (this is a demo, so I skipped iterator/paging stuff), make a form for each, and then render the forms. I also render the new-person object in case they want to add a new one.
Next I call '$r->next', which waits for the user to submit the forms. This is Continuity magic, which inverts the web app to make it more like an interactive command line prompt. You can kinda think of this line as "$input =
Finally I check to see if they submitted data for the New-Person, and if there is anything I insert it into the DB.
# Get things going
Continuity->new->loop;
Last but not least, we start the Continuity loop, which starts looking for requests and calling main() for each new session.
So I found this all pretty interesting, and it overlaps with some of my other object-rendering experiments. I could probably add to this for pulling field metadata from objects into forms and organizing subforms and so on. Maybe next time :)