Dynamic forms with HTML::FormFu
OVERVIEW
Working with forms can quickly become nasty. Two very popular form handling libraries for Catalyst are HTML::FormHandler and HTML::FormFu. Both have their pros and cons. This article shows how to use HTML::FormFu in an easy way for creating dynamic forms.
HTML::FormFu and Catalyst
What FormFu can do
HTML::FormFu calls itself a form creation, rendering and validation framework. Usually a form is constructed from a data structure describing all of the form's requirements and listing all of its fields in their displayed order. Typically the description for a form is loaded from a config file which may get stored in various formats like YAML, XML or in literal Perl syntax.
Static forms may be sufficient in most cases. If two users should be allowed to do different things or certain fields are only required under certain conditions, multiple forms may be required. Using a form containing the summary of all fields and a logic for removing or changing fields sounds like a solution at first.
HTML::FormFu provides an API for introspecting and manipulating a form. As long as the required manipulations remain simple, using this API is easy and straight forward. If, however, the number of manipulations will increase or the number of forms grow, chances are high to generate unreadable do-always-the-same-manipulation code. Within very short time you end up in 2000+ line Controller-Classes mostly doing form manipulation. But there is an easier and more readable way.
For demonstration how to use and modify forms, we will construct a hypothetical workflow of some entity. The entity will have a name, a comment and a simple workflow with the steps "create", "edit", "check" and "finish". Name and workflow may only get edited by administrators, regular users may only see these fields without editing capability.
Preparing our app
For the following examples we need a simple Catalyst application. We will not use any templates and just a single controller.
These lines will create all needed things and launch your app.
# ensure you have FormFu installed $ cpanm HTML::FormFu $ cpanm Catalyst::Controller::HTML::FormFu # create and launch app $ catalyst.pl FormDemo $ cd FormDemo $ ./script/formdemo_create.pl controller Entity $ ./script/formdemo_server -drf
A simple form
By convention, a form is located in a directory inside root/forms matching the controller's namespace having the action's name plus an extension matching the chosen file format.
In Perl syntax the form for the fields required above could look like this. Please create a file with this content and save it into root/forms/entity/edit.pl.
{ indicator => 'save', auto_fieldset => { legend => 'Edit Entity' }, elements => [ { # in case we want to save the record back to a database type => 'Hidden', name => 'entity_id', }, { type => 'Text', name => 'name', label => 'Name:', # for later expansion, see below stash => { allow_admin_only => 1 }, }, { type => 'Textarea', name => 'comment', label => 'Comment:', rows => 5, }, { type => 'Select', name => 'step_id', label => 'Status:', options => [ [ 0 => 'create' ], [ 1 => 'edit' ], [ 2 => 'check' ], [ 3 => 'finish' ], ], # for later expansion, see below stash => { allow_admin_only => 1 }, }, { type => 'Submit', name => 'save', value => 'save', }, ], }
To get the form running in our application we need a controller that extends
Catalyst::Controller::HTML::FormFu
and an action that loads the form. The
FormConfig
attribute tells our action to load the form from the matching
path. If you plan to organize your forms in a different directory layout, you
may add the relative path as an argument to the attribute like
FormConfig('directory/file.pl')
. Please read
Catalyst::Controller::HTML::FormFu
for the glory details.
package FormDemo::Controller::Entity; use Moose; BEGIN { extends 'Catalyst::Controller::HTML::FormFu' } sub begin :Private { my ($self, $c) = @_; # simulate admin switch using a GET parameter if ($c->req->params->{user_is_admin}) { $c->stash->{user_is_admin} = 1; } } sub edit :Local :FormConfig { my ($self, $c) = @_; my $form = $c->stash->{form}; # simulate a database lookup $form->default_values({ entity_id => 42, name => 'My Entity', comment => 'The quick brown fox jumps over the lazy dog', step_id => 1, }); $c->res->body($form->render); } 1;
Now, point your browser to your app and you will see a form (well, this screenshot has some CSS applied, but this is not the point).
Manipulating forms the hard way
HTML::FormFu offers methods for traversing the form's fields, searching form elements by various criteria as well as inserting, modifying and removing form elements. Doing lots of manipulations can lead to hard-to-maintain code. You will find a complete explanation of all available methods in HTML::FormFu. If your code exceeds a few lines or will contain repeated sequences please think about using a plugin for the manipulations.
Using a Plugin
To get a plugin running, you could either add a plugins
key
to every form you like to get enriched by one or more plugins. However, this
may lead to many repetitions. If a plugin is required for every form you
plan to use, it may get added inside your config. Here is an example configuration
in Perl syntax:
'Controller::HTML::FormFu' => { constructor => { plugins => [ 'ManipulateFields' ], }, },
For the purpose we intend to do, we need to find a space for storing additional
information into every single form field. HTML::FormFu offers two places
for this: attributes
and stash
. Each of both places can be used in
a form definition and queried or modified with the traversal API. If you plan
to use attributes, prepending an attribute-key with "data-" is nice in order
to maintain your HTML valid instead of inventing phantasy-HTML-attributes or
accidentally overwriting existing attributes. For this demonstration we are
using the stash.
Let us add a simple plugin which reads thru every form element, isolates stash keys and tries to call a method with the stash key's name if available.
package HTML::FormFu::Plugin::ManipulateFields; use Moose; extends 'HTML::FormFu::Plugin'; sub pre_process { my $self = shift; my $form = $self->form; my $c = $form->stash->{context}; foreach my $element ( @{ $form->get_all_elements } ) { $self->$_($c, $form, $element, delete $element->stash->{$_}) for grep { $self->can($_) } keys %{$element->stash}; } } sub allow_admin_only { my ($self, $c, $form, $element) = @_; return if $c->stash->{user_is_admin}; $element->attributes->{readonly} = 'readonly'; $element->attributes->{disabled} = 'disabled'; # if you plan to use HTML::FormFu::Model::DBIC, also add: # $element->model_config->{read_only} = 1; } 1;
Alternatives
another place to "hide" the method names to call inside your plugin is
the attributes
attribute every field provides. A field might look like
{ type => 'Text', name => 'name', label => 'Name:', attributes => { 'data-allow-admin-only => 1 }, },
The prefix "data-" was chosen to generate a valid HTML and to prevent collisions with other HTML attributes of the generated HTML markup.
Possible expansions could be to namespace the methods by using certain parts of the name as part of a class, others as the methods. Or you may allow extending your plugin by using tools like Module::Pluggable.
What to do next?
If you think about different permissions, states, situations or flags sitting at your records you will easily find situations that will quickly become candidates for form manipulations. Here are some examples
- display accounting fields for your staff
- hide prices from non-privileged people
- show additional fields for your admins
- add hint messages for certain users
- construct context-dependant selectbox options
- add a captcha for anonymous users
Just a small list of raw ideas. Would be great to read a blog entry containing more.
Caveats
A developer is free to call $form->process()
as often as he likes.
Sometimes these calls are necessary after certain kinds of form manipulation.
Every call to process
will trigger the plugin logic above again. If the
logic you apply to your form is expensive in terms of CPU or processing time
or destructive in any kind, you might consider to prevent multiple executions.
This is the reason why the name of method getting called is removed from the
form element's stash in the code examples above.
For More Information
Summary
HTML::FormFu provides a clean way to operate with forms. If you use plugins to bend your forms to the shape you need, you will keep the form handling as simple as it is and can focus on the important parts of your app.
Author
Wolfgang Kinkeldei <wolfgang [at] kinkeldei [dot] de>