Die Forms, Die!

jk2addict on 2007-03-03T02:19:09

In this day and age, it amazes me that doing forms in web apps still sucks serious butt. Sure, we have HTML::FormFu, CGI::FormBuilder, HTML::Widget, Text::FormBuilder, FormValidator::Simple, and Data::FormValidator. They're all great at what they do, but they all suck in unique ways.

C::FB is great at form rendering, from a config file even, but it's validaton is lacking (and it only supports D::FV).

FV::S is great at validation, esp getting messages from config files, but getting profiles from config files is another story, and the YAML soon gets crazy.

D::FV is also a good validator, but I'm not terribly fond at how to get all of the errors our of results and setup custom messages...from config files is worse.

H::W make me feel ill every time I use it. All code, no external config. Easy to localize labels though.

FormFu looks promising, but at this point, I'm in crunch time, and it looks a little large for my needs. It's really good at localizaiton, even using the I18N plugin in Cat to do the job.

OF course, I could just hard code the forms, and just use FV::S, but doing the profiles in YAML is ugly as sin.

So, here it is, 2 weeks of form futzing with the various tools. In the end, just writing my own just to fill my needs seems like the only sane option so I can get back to actually getting work done. Plus, it gives me what I really wnat, a sane external config for form/validation/messages and even less code in most of my controllers.

---
name: roles_edit
method: POST
javascript: 0
stylesheet: 1
sticky: 1
field_order:
  - id
  - name
  - description
  - created
fields:
  id:
    type: hidden
    force: 1
    constraints:
      - NOT_BLANK
      - INT
  name:
    type: text
    size: 20
    disabled: 1
    force: 1
    constraints:
      - NOT_BLANK
      - LENGTH, 1, 25
  description:
    type: text
    size: 50
    constraints:
      - NOT_BLANK
      - LENGTH, 1, 100
      - INT
  created:
    size: 20
    disabled: 1


A litte bit of FB. A little bit of FV::S. A little bit of D::FV. And no ugly nested nested arrays of lists. A little controller code to split that up into the various data bits for FB, D::FV and we're off to the races.


Re: Die Forms, Die!

fireartist on 2007-03-03T08:30:19

while that syntax is nice and clean, it's just a little too simple - because 'fields' is a hashref, and not an arrayref, it means that the order of fields is not guaranteed - which isn't great for a consistent user interface ;)

$ ./local/bin/perl -MYAML::Syck=LoadFile -MData::Dumper -le '
my $x = LoadFile( q{test.yaml} );
print for keys %{ $x->{fields} };

'
created
name
id
description

Re: Die Forms, Die!

fireartist on 2007-03-03T08:56:52

Is my face red?

Sorry, I completely missed the 'field_order' section!

That's interesting, though when it comes to bigger forms I don't think I'd like having all field names repeated like that.

Re: Die Forms, Die!

jk2addict on 2007-03-03T17:56:30

Easy enough to fix I guess, just move fields as an array instead of a hash. Either way, it's small, and works for me for now. And since it's still mostly options based on FB, and FV::S, converting to something else later will be pretty easy.

One thing I didn't mention is the messages and labels. If no message or label is supplid, they default to LABEL_$FIELDNAME and $FIELDNAME_$CONSTRAINNAME.

Since those are just keys I use in I18N lexicons, all is well.

Re: breaks down on complicated validation

brian_d_foy on 2007-03-03T19:07:29

I've been thinking about this sort of thing a lot, lately. I don't have the answers yet (or maybe ever), but I tried this approach and abandoned it.

  • There's a mix of model and view here, and that coupling sometimes causes problems. But, sometimes you have to let that slide if you want to get things into the javascript.
  • Although you can do low level validation (NOT_BLANK), you can't express complex, multi-field relationships. Take, for instance, a date use case I recently wrangled for a client. First it must be a valid date, but it might also have to be before another date field. Now, you only need that other date field if the date range option is checked because the range is otherwise unbounded. Then, to make it really complicated, that date range is restricted by a fourth field that describes a type of action that is only valid for certain date ranges. Data::FormValidator ultimately choked on this, and after looking at the code, I gave up any hope of refactoring it.


It's a big mess, and from what I've seen on CPAN people haven't really considered a complicated situation in their designs. The interfaces work well for simple cases, but quickly become unworkable.

Re: breaks down on complicated validation

fireartist on 2007-03-08T11:02:07

In HTML::Widget, you can create a 'callback' constraint which can validate multiple input fields at once.
For example,

$form->constraint( Callback => 'foo', 'bar' )
    ->callback( \&sub );
The &sub() will receive the input values for both 'foo' and 'bar' as its arguments.

The HTML::FormFu module http://code.google.com/p/html-formfu/ mentioned by the OP is still under heavy development, and I have a proposal that should provide enough flexibility to solve your problem more easily than the above-mentioned callback.

I posted the full proposal to the html-widget mailing list http://lists.rawmode.org/pipermail/html-widget/2007-March/000479.html
copied below are the most relevant points...

The old HTML-Widget process() simply ran all constraints, then all
filters, and that was your lot.

FormFu currently goes a step further with:
    * constraints
    * filters
    * inflators

I propose making it a lot more powerful and flexible, with the following steps:
    * filter
    * constrain
    * inflate
    * validate
    * transform

Any of these steps (except 'filter') will be able to throw an exception, which will be displayed to the user as a field error.
Each step will only proceed if there are no errors from previous steps.

The first filter for each field will be fed the raw input for that field only, and it's output will be piped to the next filter for that field.
Likewise, with 'inflate', the output is piped from one inflate handler to the next.
The same goes for 'transform', but the first transform handler will receive the inflated value.

The purpose of the filter handlers would be to cleanup input before validation, so you might strip leading and trailing whitespace with the 'TrimEdges' filter. Or you might use the 'NonNumeric' filter to remove any spaces or hyphens entered into a credit card number field.

The purpose of the 'constrain' handlers would be to check such low-level things such as "is this in range?", "does this contain valid characters?" or "is this an email address?".

The purpose of the 'inflate' handlers would be to, for example, turn a date into a DateTime object, or turn a file upload into an Imager object.

The purpose of the 'validate' handlers would be to check higher-level (business) rules, and they will have access to the inflated values of all fields. So you could check, for example, "is this date after that date?" or "is 'c' only filled in if 'a' and 'b' are?".

Because these are likely to contain more-complex logic, it would be expected that the user would create a new subclass of HTML::Widget::Validate for each business rule, which would contain the programmatic logic. So in your form setup, you only need refer to the validation handler by name (otherwise, you'd end up trying to program in yaml - nasty!).

And, of course, if you're using Catalyst::Controller::HTML::FormFu, your validation code will have access to the catalyst context via the form's stash.

$self->form->stash->{context}
The 'transform' handler is provided as a further hook to do anything else necessary after all validation is complete.

So, to take the example of John Napiorkowski's HTML-Widget filters which use Imager to resize uploaded image files to a standard size...

I would suggest there should be a HTML::FormFu::Inflate::Imager which simply reads an uploaded file and returns an Imager object.
There would then be a HTML::FormFu::Transform::Imager which allows you to call methods on that Imager object.
The yaml config for a field might be something like:

---
element:
    - type: file
        name: avatar
        inflate:
            - Imager
        transform:
            - type: Imager
                scaleX: [pixels, 100]