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>