Writing Forms¶
Introduction¶
Since, in the Plone Intranet project, we are following a design driven model, we cannot rely on a form generating library (such as z3c.form) to generate our forms for us, as the markup generated by this library does not conform to the markup in the design prototype.
Instead of using z3c.form to render our forms, we hand code our forms based on the markup found in the prototype.
We do however want to stay as close and true as possible to existing development patterns used in Plone. Therefore, we create custom Dexterity add/edit views for all Dexterity content types, and put the prototype markup in their templates.
Even though forms are hand-coded (by the designer), form submission, handling and content type creation/modification are all still handled by Dexterity/z3c.form.
However, in order to have z3c.form handle our submitted forms, we need to add
some extra markup (such as special name
attributes to our input
elements) to our custom form markup.
Custom Add/Edit views for Dexterity content types¶
Each Add or Edit view consists of two parts. The form and a view which wraps it in a specific layout (also called a form wrapper).
Custom view (form wrapper) layout¶
Generally you wouldn’t care about the form wrapper view’s layout, but in the case of Plone Intranet, we need fully custom designer specified markup, so it was necessary to modify this layout.
Luckily, this only needed to be done once, and then all custom add/edit views will reuse it.
For posterity, here’s how it’s done.
You need to register a layout_factory
adapter. See:
ploneintranet/theme/browser/templates.py
This adapter has a layout template:
ploneintranet/theme/browser/templates/layout.pt
and a corresponding ZCML registration snippet:
ploneintranet/theme/browser/configure.zcml
The form¶
The forms are subclasses of the default Dexterity add and edit forms, together
with a mixin class DexterityFormMixin
(the point of this mixin class is
explained in the templates section:
class AddForm(add.DefaultAddForm, views.DexterityFormMixin):
""" Custom add form for the Document content type.
"""
template = ViewPageTemplateFile('templates/edit_document.pt')
Additionally, for each form we need to provide a custom template which contains the markup from the prototype.
In the example above, we specify the custom template as an attribute on the class.
Custom marshalling of data¶
In computer science the act of “marshalling” refers to transforming a certain representation of data into a different format suitable for transmission or computation.
z3c.form expects the form request data to conform to certain expectations, such
as that the keys of the request values need to have certain specific values,
like form.widgets.IDublinCore.title
for the title field.
We can fix this either by modifying the template markup (e.g. by adding a
custom name
attribute on the title
widget with value form.widgets.IDublinCore.title
)
or we can handle it server side in the extractData
method:
def extractData(self, setErrors=True):
exclude_from_nav = ''
if not self.request.get(exclude_from_nav):
# XXX: This is a required field, but not in the form. Not yet sure
# what the right approach to deal with it is.
# Either we deal with it here, or add a hidden field in the
# template.
self.request.form[exclude_from_nav] = 'selected'
return super(AddForm, self).extractData()
The view (aka form wrapper)¶
For each form (i.e. add and edit), we need a view which acts as a form wrapper:
class EditView(edit.DefaultEditView):
""" Custom edit view for the Document content type.
"""
form = EditForm
The view subclasses the default add and edit views from plone.dexterity
.
Each view also requires a form
attribute to be set which points to the form
created in the section above.
Register add/edit views with zcml¶
The views need to be registered in zcml (in ploneintranet.theme/browser/configure.zcml
).
The add view is a special case, since it’s an adapter on the ++add++
namespace:
<adapter
for="Products.CMFCore.interfaces.IFolderish
..interfaces.IThemeSpecific
plone.dexterity.interfaces.IDexterityFTI"
factory=".document.AddView"
name="Document"
provides="zope.publisher.interfaces.browser.IBrowserPage"
/>
Important parts to note is the factory
directive, which points to the view
class as well as the second interface passed to the for
directive, which is
the theme layer for ploneintranet.theme
.
The edit view is registered like a normal browser page:
<browser:page
for="plone.app.contenttypes.interfaces.IDocument"
name="edit"
class=".document.EditView"
permission="cmf.ModifyPortalContent"
/>
Templates¶
Widget markup¶
The markup in the templates should come straight from the prototype (i.e. design driven).
However, in order to have z3c.form handler form submission, we need z3c.form to
render the form buttons and also add special name
attributes to our form
inputs.
Additionally, for dynamic rendering, we need to sprinkle the markup with TAL
statements,
which won’t be found in the prototype.
For example:
<fieldset class="horizontal">
<label>Title <sup class="required">*</sup> <a href="tooltip-help.html#subject-2" class="iconified icon-info-circle help pat-tooltip" data-pat-tooltip="trigger: click; source: ajax">More info</a>
<input type="text"
name="form.widgets.IDublinCore.title"
tal:attributes="value view/title|nothing"/>
</label>
</fieldset>
Note the name
attribute which has a value of form.widgets.IDublinCore.title
.
The input needs this name
attribute for z3c.form to properly parse the
request.
To get the correct name
attribute, I inspect a vanilla Plone form of the same content type.
Note also the tal:attributes
statement which ensures that the widget will
have the correct default value set.
The fact that view/title
(or python:view['title']
) can be called on the form,
is due to the DexterityFormMixin
class mentioned above. This class provides a custom
__getitem__
method which will look up the requested attribute, first in the
request, and then on the context (in the case of edit forms).
Unused but required fields¶
If your content type has required fields, which do not have widgets in the prototype, you’ll need to still add them to the form as hidden inputs, otherwise z3c.form will complain that they aren’t set:
<!-- XXX: This is a required field, but will not be used in Plone Intranet -->
<input type="hidden" value="selected" name="form.widgets.IExcludeFromNavigation.exclude_from_nav"/>
Form buttons¶
z3c.form handles form submission via so-called button handlers. In order for this to work, the buttons need special markup.
One way to do this, would be to manually edit the markup from the prototype, so
that it has the necessary attributes that z3c.form expects (likely only
name
).
However, I’ve overridden the default button markup from z3c.form (see
ploneintranet/theme/browser/templates/submit_input.pt
) to match the proto’s
markup.
Here’s an example how to render buttons via z3c.form:
<fieldset class="button-bar group">
<tal:block repeat="action view/actions/values">
<button type="submit" tal:replace="structure action/render" />
</tal:block>
</fieldset>