For anyone wondering why I've been somewhat quieter than normal... we
had guests over the weekend, and...

There has been a lot of activity around form generation lately, which
has been very helpful. There are many different ideas floating around
out there, and those have given me lots of food for thought for how to
handle form generation and, ultimately, CRUD in TurboGears.

Today, I checked in the first bit of code for TurboGears' form
generator. (Aside: if you have any thoughts on naming for the forms
package, that'd be good. turbogears.forms seems lame.)

There's a fair bit more to my thoughts on this subject than what
currently appears in the code, but there's enough there that I wanted
to talk about it, start getting some more opinions, and see where
people might want to help out.

Rolling Our Own?
----------------

TurboGears has become famous as a "megaframework", integrating other
tools to do the bulk of its work. With so much going on in form
generation, why create a new package?

Before I start hearing calls of "Not Invented Here" syndrome, I'll
point out that I've already started working on deprecating TestGears
in favor of Nose. Believe me, I want to work on apps and I'm happy to
get all of the assistance I can in building out the framework to
create useful apps.

Creating and dealing with forms is an crucial part of making a web
application. Moving data to and from forms is a big part of a web app,
and making that easy is important. CherryPy's method of taking
incoming parameters and mapping them to method parameters is really
nice, but it's only a piece of dealing with forms.

Because dealing with forms is so central to a web app, assistance with
those forms will be an integral part of TurboGears. This is why we're
rolling our own forms system... where else will we find a forms
package that tightly integrates with turbogears.expose, uses Kid for
templating and helps out with SQLObject? MochiKit will likely get into
the act in short order as well.

Rolling our own forms package doesn't mean ignore what's out there.
The FormEncode validators play an important part. But, beyond that,
design ideas are coming from all over the place. We might not be using
all of the code that exists out there, but I do hope we're learning
from the choices people have previously made.

Code Generation vs. API
-----------------------

Rails takes a very simple, yet still effective approach to
customization of automatically created forms: you generate the code
and the modify the files. This is effective, because some kind of
customization is nearly always required. Of course, it also has the
drawbacks of any code generation system: what happens when you would
need to regenerate the code because of other changes in the system?

Rails does go a bit beyond code generation, though. It provides
functions that you call for generating text fields, etc.

What I'm shooting for is a system that provides easy and flexible
customization in such a way that you can realistically use the
automatic API with minimal tweaks. This will help keep your UI in sync
with your code.

At the same time, I'm looking to offer code generation as well, for
people who need absolute and total customization.

Widgets
-------

The key to this is the widgets. Widgets live in Python code but are
rendered by Kid templates. Right now, there's only a couple of them
and I'm not going to write much about them, because there are some
parts of the API that are not fully hashed out yet. You could start
writing widgets right now, if you wish, but you'd likely have to
rewrite bits as the API fills in.

So, with the teaser that widgets are the key, I'm going to stop
talking about widgets for now.

Forms
-----

Instead, I'll talk a bit about forms. A form is actually a widget, but
it has special logic for dealing with a collection of widgets that it
holds on to.

You can instantiate a form like this:
myform = TableForm(widgets=[
                widgets.TextField("name"),
                widgets.TextField("address"),
                widgets.TextField("age", default=0,
                                validator=validators.Int())])

A TableForm displays a label for each field in one column of a table,
with the field controls in another column of the table. A TableForm
gives you a submit button for free. As you can see from this example,
a widget can have a default value and widgets can have validators.
Validators are important because they can handle complex
back-and-forth between Python and the web.

When you want to display the form, you can do something like this:

@turbogears.expose(html="fancy.template")
def index(self):
    return dict(form=myform)

Then, in the template, you would do something like this:
<div py:replace="form.insert(action='save')"/>

Voila! You have a form.

Actually, this API is going to change subtly. There will be an expose
parameter to specify forms that are present in the template. Since we
gave the form an action of "save", we should define a save method:

@turbogears.expose()
def save(self, name, address, age):

There are two problems with this definition, though:
1) what happens to the incoming submit button?
2) shouldn't age be validated?

expose now has a new input_form parameter.

@turbogears.expose(input_form=myform)
def save(self, name, address, age):

By specifying that data is coming from that form, the widgets will
each get a chance to validate the incoming data and convert it from
whatever web format it was in to a convenient Python parameter. "age",
when it arrives, will be an int. Also, since the form knows what the
submit button was called, it is automatically thrown out. (Someone's
probably going to say "hey! I need that submit button value!"...
widgets are customizable through standard Python means. Subclass and
override. Piece of cake.)

What if the user entered "forty-two" for their age? Well, the
validation will fail. As declared above, an attempt would be made to
call validation_error, otherwise an exception is raised. Since the use
of the validation_error method has been painful, I've added another
approach to error handling.

@turbogears.expose(input_form=myform)
def save(self, name, address, age, has_errors):

By adding a has_errors parameter, you're signaling to TurboGears that
this method is prepared to deal with validation issues that come up.
TurboGears automatically throws out the invalid values and will pass
in None in their place. In this example, age would be None.

The validation errors that came up can be found in
cherrypy.request.form_errors. The valid values from the input form are
stored in cherrypy.request.input_values. You could probably do
productive things with those two values but, luckily, you don't have
to. To handle that problem with the "age" input:

@turbogears.expose(input_form=myform)
def save(self, name, address, age, has_errors):
    if has_errors:
        return self.index()

The form will be redisplayed with good values prepopulated and the
invalid ones thrown out. An error message would be displayed next to
the "age" field on the form. And we all heave a sigh of relief that we
didn't have to do that in validation_error :)

That's It For Today
-------------------

I'd welcome comments on any of this. This part of things is fairly
well baked, so we can reasonably discuss this in a mediumly-trafficked
google group of 550+ people.

If you find that you have some time on your hands over the next couple
of days and want to work on this a bit, send me a private email. I
think there are some areas where the work can be split off without
stepping all over each others' toes.

Once the widgets are more fully baked (tomorrow, hopefully), there
will be lots of room for people to contribute work on widgets!

--
Kevin Dangoor
Author of the Zesty News RSS newsreader

email: [EMAIL PROTECTED]
company: http://www.BlazingThings.com
blog: http://www.BlueSkyOnMars.com

Reply via email to