Lab notes :: Pylons :: Pylons docstutorials :: :: Using Mako to present and handle forms
Using Mako to present and handle forms
Using Mako to present and handle forms¶
(Nicked from Mike’s blog entry)
In developing Mako, a primary goal was to make a super-nice version of a particular “component” pattern which I had used for years primarily with HTML::Mason which for me provides a “sweet spot” of obviousness, agility, and succinctness. The focus is around the ability to create “tag libraries” which interact easily with a server-parsed templating language, and which can be implemented within templates themselves. In JSP development, taglibs are now the standard way to indicate dynamic areas of templates, but while they look pretty clean, they are painful to implement (requiring HTML embedded in hand-crafted classes, a few dozen XML pushups for every tag you add, and the obligatory application restart whenever they change), and the EL and OGNL expressions which are standard within taglibs interact terribly with straight Java code.
Mako allows the creation of tags which can be arbitrarily nested and interactive with one another via the <%def> construct, in combination with the <%call> tag. It’s been my observation that the <%call> tag as well as the usage of nesting-aware <%defs> hasn’t caught on yet, as the examples in the docs are a little dense, so here I will seek to demystify it a bit.
Pylons currently recommends a decent approach to rendering forms, using Form Tags which are essentially little functions you can embed in your template to render standard form elements. The handling of the form at the controller level uses FormEncode and routes validation errors through htmlfill. My approach modifies this to use Mako tags to build a site-specific taglib around the webhelpers tags and adds an explicit interaction between those tags and the controller, in a manner similar to a Struts form handler, which replaces htmlfill and allows all layout, including the layout of validation error messages, using the same template system. It also adds a preprocessor that illustrates how to build custom tags in Mako which look as nice as the built-in ones.
A tar.gz of the approach, which at this point should be regarded strictly as a proof of concept, can be downloaded here (works against Pylons 0.9.7), which contains three templates each illustrating a different approach to laying out the form. The three approaches are raw webhelpers with htmlfill, the <%call> tag approach, and finally using the “custom tag” approach. The final result, present in the file templates/mako_helpers.html, looks like this:
<%namespace name="form" file="/form_tags.mako"/>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Mako Form Helpers</title>
<link rel="stylesheet" href="/style.css" type="text/css" />
</head>
<body>
<h3>Using Mako Helpers</h3>
<%form:form controller='comment' action='mako_post'>
<table>
<tr>
<th colspan="2">Submit your Comment</th>
</tr>
<tr>
<td>Your Name:</td>
<td><%form:text name="name"/></td>
</tr>
<tr>
<td>How did you hear about this site ?</td>
<td>
<%form:select name="heard">
<%form:option value="" selected="True">None</%form:option>
% for desc, value in c.heard_choices:
<%form:option value="${value}">${desc}</%form:option>
% endfor
</%form:select>
</td>
</tr>
<tr>
<td>Comment:</td>
<td><%form:textarea name="comment"/></td>
</tr>
<tr>
<td colspan="2"><%form:submit/></td>
</tr>
</table>
</%form:form>
</body>
</html>
Where of note are the Mako-like <%form:foo> tags that aren’t part of Mako ! A short preprocessor is applied to the source file which turns a tag like <%form:foo> into <%call expr="foo()"> at template compile time. I.e., a tag used above as:
<%form:textarea name="comment"/>
is preprocessed into raw Mako code:
<%call expr="form.textarea(name='comment')"/>
The %call tag invokes the textarea def inside the form namespace, which is defined in the file form_tags.mako. The def for textarea looks like:
<%def name="textarea(name, default=None, **attrs)">\
<%doc>
Render an HTML <textarea></textarea> tag pair with embedded content.
</%doc>\
${form_errors(name)}\
${h.textarea(name, content=request.params.get(name, default), **attrs)}\
</%def>
Above, the $form_errors(name) is a def call used for reporting validation messages. The point of form_tags.mako is that all the form tags and their layout is plainly visible and easily customized. Multiple versions of the file can be used in one application, providing different form layouts for different areas. The fact that it uses the h helper to render the actual HTML for each form control is also arbitrary; you could just as well implement the <textarea> source directly within the def if some special treatment were needed.
The demo also contains a modified version of Pylons’ @validate decorator. Usage is mostly the same, except the form parameter is replaced by the more direct input_controller parameter, which is the method used for input:
from formhelpers.lib.mako_forms import validate as mako_validate
class CommentController(BaseController):
def index(self):
return render('/mako_helpers.html')
@mako_validate(schema=CommentForm, input_controller=index)
def mako_post(self):
c.name = self.form_result['name']
return render('/thanks.html')
Where above, mako_post hits the validator, and on an invalid exception the index controller method is called instead. Validation errors are placed in self.form_errors as well as c.form_errors for template access. The validator as well as the preprocessor are defined in lib/mako_forms.py.
Update¶
I’ve refined the formhelpers demo with a @validate function tailored to a single usage pattern (download). Each form now has a name, which identifies its place on c, such as:
<%form:form name="comment_form" controller='comment' action='post'>
The controller now places a comment_form dictionary on c in all cases, which the @validate function takes care of on the post side:
@validate("comment_form", CommentForm, input_controller=index)
def post(self):
c.name = self.form_result['name']
return render('/thanks.html')
The hacky methodology of relating the <%form:option> tag to its parent <%form:select> tag has also been replaced with something reasonable.
This version of formhelpers is production ready.
As far as comments regarding @rest.dispatch_on and forcing a GET to accommodate it, I think that’s kind of ugly since I don’t really believe in “virtual requests”, which is one reason I rewrote the decorator in the first place (i.e., to remove the “re-get” aspect of it). If you are dual-purposing your index() method to display the form on GET as well as to re-display on POST, then index() needs to accommodate both methods. Otherwise you can break func:index out into func:index and _display_index() and have _display_index() be your input_controller.
© Copyright 2008, Ben Bangert, James Gardner, Phil Jenvey.