Widgets

Introduction

A widget is a bit of code that is willing to:

  • serialize a cstruct into HTML for display in a form rendering
  • deserialize data obtained from a form post (a pstruct) into a data structure suitable for deserialization by a schema node (a cstruct).
  • handle validation errors

Deform ships with a number of built-in widgets. You hopefully need not create your own widget unless you try to do something that the built-in widget set did not anticipate. However, when a built-in Deform widget does not do exactly what you want, you can extend Deform by creating a new widget that is more suitable for the task.

Widget Templates

A widget need not use a template file, but each of the built-in widgets does. A template is usually assigned to a default widget via its template and readonly_template attributes. Those attributes are then used in the serialize method of the widget, as shown in the following.

1
2
3
4
5
def serialize(self, field, cstruct, readonly=False):
    if cstruct in (null, None):
        cstruct = ''
    template = readonly and self.readonly_template or self.template
    return field.renderer(template, field=field, cstruct=cstruct)

The deform.field.renderer() method is a method which accepts a logical template name (such as texinput) and renders it using the active Deform renderer. The default renderer is the ZPT renderer, which uses the templates within the deform/templates directory within the deform package. See Templates for more information about widget templates.

Widget JavaScript

Some built-in Deform widgets require JavaScript. In order for the built-in Deform widgets that require JavaScript to function properly, the deform.load() JavaScript function must be called when the page containing a form is renderered.

Some built-in Deform widgets include JavaScript which operates against a local input element when it is loaded. For example, the deform.widget.AutocompleteInputWidget template looks like the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<span tal:define="name name|field.name;
                  css_class css_class|field.widget.css_class;
                  oid oid|field.oid;
                  style style|field.widget.style;
                  autofocus autofocus|field.autofocus"
      tal:omit-tag="">
    <input type="text"
           name="${name}"
           value="${cstruct}"
           data-provide="typeahead"
           tal:attributes="class string: form-control ${css_class or ''};
                           style style;
                           autofocus autofocus;
                           attributes|field.widget.attributes|{};"
           id="${oid}"/>
    <script tal:condition="field.widget.values" type="text/javascript">
        deform.addCallback(
          '${field.oid}',
          function (oid) {
              $('#' + oid).typeahead(${structure:options});
              if("${autofocus}" == "autofocus") {
                $('#' + oid).focus();
              }
          }
        );
    </script>
</span>

field.oid refers to the ordered identifier that Deform gives to each field widget rendering. You can see that the script, which runs when this widget is included in a rendering, calls a function named deform.addCallback, passing it the value of field.oid and a callback function as oid and callback respectively. When it is executed, the callback function calls the autocomplete method of the jQuery selector result for $('#' + oid).

The callback defined above will be called under two circumstances:

  • When the page first loads and the deform.load() JavaScript function is called.
  • When a sequence is involved, and a sequence item is added, resulting in a call to the deform.addSequenceItem() JavaScript function.

The reason that default Deform widgets call deform.addCallback rather than simply using ${field.oid} directly in the rendered script is because sequence item handling happens entirely client side by cloning an existing prototype node, and before a sequence item can be added, all of the id attributes in the HTML that make up the field must be changed to be unique. The addCallback indirection assures that the callback is executed with the modified oid rather than the protoype node's oid. Your widgets should do the same if they are meant to be used as part of sequences.

Widget Requirements and Resources

Some widgets require external resources to work properly (such as CSS and JavaScript files). Deform provides mechanisms that will allow you to determine which resources are required by a particular form rendering, so that your application may include them in the HEAD of the page which includes the rendered form.

The (Low-Level) deform.Field.get_widget_requirements() Method

After a form has been fully populated with widgets, the deform.Field.get_widget_requirements() method called on the form object will return a sequence of two-tuples. When a non-empty sequence is returned by deform.Field.get_widget_requirements(), it means that one or more CSS or JavaScript resources will need to be loaded by the page performing the form rendering in order for some widget on the page to function properly.

The first element in each two-tuple represents a requirement name. It represents a logical reference to one or more JavaScript or CSS resources. The second element in each two-tuple is the requested version of the requirement. It may be None, in which case the version required is unspecified. When the version required is unspecified, a default version of the resource set will be chosen.

The requirement name/version pair implies a set of resources, but it is not a URL, nor is it a filename or a filename prefix. The caller of deform.Field.get_widget_requirements() must use the resource names returned as logical references. For example, if the requirement name is jquery, and the version id is 2.0.3, the caller can take that to mean that the jQuery library should be loaded within the page header via, for example the inclusion of the HTML <script type="text/javascript" src="https://deformdemo.pylonsproject.org/static/scripts/jquery-2.0.3.min.js"></script> within the HEAD tag of the rendered HTML page.

Users will almost certainly prefer to use the deform.Field.get_widget_resources() API (explained in the succeeding section) to obtain a fully expanded list of relative resource paths required by a form rendering. deform.Field.get_widget_requirements(), however, may be used if custom requirement name to resource mappings need to be done without the help of a resource registry.

See also the description of requirements in deform.Widget.

The (High-Level) deform.Field.get_widget_resources() Method

A mechanism to resolve the requirements of a form into relative resource filenames exists as the method deform.Field.get_widget_resources().

Note

Because Deform is framework-agnostic, this method only reports to its caller the resource paths required for a successful form rendering, it does not (cannot) arrange for the reported requirements to be satisfied in a page rendering. Satisfying these requirements is the responsibility of the calling code.

The deform.Field.get_widget_resources() method returns a dictionary with two keys: js and css. The value related to each key in the dictionary is a list of relative resource names. Each resource name is assumed to be relative to the static directory which houses your application's Deform resources (usually a copy of the static directory inside the Deform package). If the method is called with no arguments, it will return a dictionary in the same form representing resources it believes are required by the current form. If it is called with a set of requirements (the value returned by the deform.Field.get_widget_requirements() method), it will attempt to resolve the requirements passed to it. You might use it like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import deform

form = deform.Form(someschema)
resources = form.get_widget_resources()
js_resources = resources['js']
css_resources = resources['css']
js_links = [ 'http://my.static.place/%s' % r for r in js_resources ]
css_links = [ 'http://my.static.place/%s' % r for r in css_resources ]
js_tags = ['<script type="text/javascript" src="%s"></script>' % link
           for link in js_links]
css_tags = ['<link rel="stylesheet" href="%s"/>' % link
           for link in css_links]
tags = js_tags + css_tags
return {'form':form.render(), 'tags':tags}

The template rendering the return value would need to make sense of "tags" (it would inject them wholesale into the HEAD). Obviously, other strategies for rendering HEAD tags can be devised using the result of get_widget_resources. This is just an example.

deform.Field.get_widget_resources() uses a resource registry to map requirement names to resource paths. If deform.Field.get_widget_resources() cannot resolve a requirement name, or it cannot find a set of resources related to the supplied version of the requirement name, an ValueError will be raised. When this happens, it means that the resource registry associated with the form cannot resolve a requirement name or version. When this happens, a resource registry that knows about the requirement will need to be associated with the form explicitly, as shown in the following code sample.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
registry = deform.widget.ResourceRegistry()
registry.set_js_resources('requirement', 'ver', 'bar.js', 'baz.js')
registry.set_css_resources('requirement', 'ver', 'foo.css', 'baz.css')

form = Form(schema, resource_registry=registry)
resources = form.get_widget_resources()
js_resources = resources['js']
css_resources = resources['css']
js_links = [ 'http://my.static.place/%s' % r for r in js_resources ]
css_links = [ 'http://my.static.place/%s' % r for r in css_resources ]
js_tags = ['<script type="text/javascript" src="%s"></script>' % link
           for link in js_links]
css_tags = ['<link type="text/css" href="%s"/>' % link
           for link in css_links]
tags = js_tags + css_tags
return {'form':form.render(), 'tags':tags}

An alternate default resource registry can be associated with all forms by calling the deform.Field.set_default_resource_registry() class method:

1
2
3
4
registry = deform.widget.ResourceRegistry()
registry.set_js_resources('requirement', 'ver', 'bar.js', 'baz.js')
registry.set_css_resources('requirement', 'ver', 'foo.css', 'baz.css')
Form.set_default_resource_registry(registry)

This will result in the registry registry being used as the default resource registry for all form instances created after the call to set_default_resource_registry, hopefully allowing resource resolution to work properly again.

See also the documentation of the resource_registry argument in deform.Field and the documentation of deform.widget.ResourceRegistry.

Specifying Widget Requirements

When instantiating a new widget, you may specify its requirements by using the requirements attribute. The requirements are specified as a sequence of two-tuples, dicts, or a combination of both two-tuples and dicts.

The two-tuple form uses the resource registry and is used by most of the core Deform widgets. The two-tuple form takes advantage of Deform's abstraction layer through the resource registry.

The dict form bypasses the resource registry. The dict form may be easier to implement than the two-tuple form for custom widgets because it does not introduce an implicit dependency on the resource registry. This is especially true if the required resources are tightly coupled to a custom widget.

Using two-tuples for specifying widget requirements

1
2
3
4
from deform.widget import Widget

class MyWidget(Widget):
   requirements = ( ("jquery", "1.4.2"), )

There are no hard-and-fast rules about the composition of a requirement name. Your widget's docstring should explain what its requirement names mean, and how to map the logical requirement name to resource paths within a resource registry. For example, your docstring might have text such as the following.

This widget uses a library name of jquery.tools in its requirements list. The name jquery.tools implies that the jQuery Tools library must be loaded before rendering the HTML page containing any form which uses this widget. jQuery Tools depends on jQuery, so jQuery should also be loaded. The widget expects jQuery Tools version X.X (as specified in the version field), which expects jQuery version X.X to be loaded previously.

It might go on to explain that a set of resources need to be added to a resource registry in order to resolve the logical jquery.tools name to a set of relative resource paths, and that the resulting custom resource registry should be used when constructing the form. The default resource registry (deform.widget.resource_registry) does not contain resource mappings for your newly-created requirement.

Using dicts for specifying widget requirements

Requirements specified as a sequence of dicts should be in the form``({requirement_type: requirement_location(s)}, ...)``. The requirement_type key must be either js or css. The requirement_location(s) value must be either a string or a list of strings. Each string must resolve to a concrete resource.

1
2
3
4
5
6
7
8
9
from deform.widget import Widget

class MyWidget(Widget):
   requirements = ( {
       "js": "my:static/path/to/jquery.js",
       "css": [
            "my:static/path/to/jquery.css",
            "my:static:path/to/bootstrap.css"],
    } )

The supplied paths are resolved by request.get_path() so the required static resources should be included in the Pyramid config.

Writing Your Own Widget

Writing a Deform widget means creating an object that supplies the notional Widget interface, which is described in the deform.widget.Widget class documentation. The easiest way to create something that implements this interface is to create a class which inherits directly from the deform.widget.Widget class itself.

The deform.widget.Widget class has a concrete implementation of a constructor and the handle_error method as well as default values for all required attributes. The deform.widget.Widget class also has abstract implementations of serialize and deserialize each of which which raises a NotImplementedError exception. These must be overridden by your subclass. You may also optionally override the handle_error method of the base class.

For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 from deform.widget import Widget

 class MyInputWidget(Widget):
     def serialize(self, field, cstruct=None, readonly=False):
         # ...

     def deserialize(self, field, pstruct=None):
         # ...

     def handle_error(self, field, error):
         # ...

We describe the serialize, deserialize and handle_error methods below.

The serialize Method

The serialize method of a widget must serialize a cstruct value to an HTML form rendering. A cstruct value is the value which results from a Colander schema serialization for the schema node associated with this widget. The result of this method should always be a unicode type containing some HTML.

The field argument passed to serialize is the field object to which this widget is attached. Because a field object itself has a reference to the widget it uses (as field.widget), the field object is passed to the serialize method of the widget, rather than the widget having a field attribute in order to avoid a circular reference.

If the readonly argument passed to serialize is True, it indicates that the result of this serialization should be a read-only rendering (no active form controls) of the cstruct data to HTML.

Let us pretend our new MyInputWidget only needs to create a text input control during serialization. Its serialize method might get defined as so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 from deform.widget import Widget
 from colander import null
 import cgi

 class MyInputWidget(Widget):
     def serialize(self, field, cstruct=None, readonly=False):
         if cstruct is null:
             cstruct = u''
         quoted = cgi.escape(cstruct, quote='"')
         return ('<input type="text" name="%s" value="%s">' %
                 (field.name, quoted))

Note that every serialize method is responsible for returning a serialization, no matter whether it is provided data by its caller or not. Usually, the value of cstruct will contain appropriate data that can be used directly by the widget's rendering logic. But sometimes it will be colander.null. It will be colander.null when a form which uses this widget is serialized without any data, for example, in an "add form".

All widgets must check if the value passed as cstruct is the colander.null sentinel value during serialize. Widgets are responsible for handling this eventuality, often by serializing a logically "empty" value.

Regardless of how the widget attempts to compute the default value, it must still be able to return a rendering when cstruct is colander.null. In the example case above, the widget uses the empty string as the cstruct value, which is appropriate for this type of "scalar" input widget. For a more "structural" kind of widget, the default might be something else, such as an empty dictionary or list.

The MyInputWidget we created in the example does not use a template. Any widget may use a template, but using one is not required. Whether a particular widget uses a template is really none of Deform's business. Deform simply expects a widget to return a Unicode object containing HTML from the widget's serialize method. It does not care how the widget creates that Unicode object.

Each of the built-in Deform widgets (the widget implementations in deform.widget) happens to use a template in order to make it easier for people to override how each widget looks when rendered without needing to change Deform-internal Python code. Instead of needing to change the Python code related to the widget itself, users of the built-in widgets can often perform enough customization by replacing the template associated with the built-in widget implementation. However, this is purely a convenience. Templates are largely a built-in widget set implementation detail, not an integral part of the core Deform framework.

Note that "scalar" widgets (widgets which represent a single value as opposed to a collection of values) are not responsible for providing "page furniture" such as a "Required" label or a surrounding <div> which is used to provide error information when validation fails. This is the responsibility of the "structural" widget which is associated with the parent field of the scalar widget's field (the "parent widget"). The parent widget is usually one of deform.widget.MappingWidget or deform.widget.SequenceWidget.

The deserialize Method

The deserialize method of a widget must deserialize a pstruct value to a cstruct value and return the cstruct value. The pstruct argument is a value resulting from the parse method of the Peppercorn package. The field argument is the field object to which this widget is attached.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 from deform.widget import Widget
 from colander import null
 import cgi

 class MyInputWidget(Widget):
     def serialize(self, field, cstruct, readonly=False):
         if cstruct is null:
             cstruct = u''
         quoted = cgi.escape(cstruct, quote='"')
         return ('<input type="text" name="%s" value="%s">' %
                 (field.name, quoted))

     def deserialize(self, field, pstruct):
         if pstruct is null:
             return null
         return pstruct

Note that the deserialize method of a widget must, like serialize, deal with the possibility of being handed a colander.null value. colander.null will be passed to the widget when a value is missing from the pstruct. The widget usually handles being passed a colander.null value in deserialize by returning colander.null`, which signifies to the underlying schema that the default value for the schema node should be used if it exists.

The only other real constraint of the deserialize method is that the serialize method must be able to reserialize the return value of deserialize.

The handle_error Method

The deform.widget.Widget class already has a suitable implementation; if you subclass from deform.widget.Widget, overriding the default implementation is not necessary unless you need special error-handling behavior.

Here is an implementation of the deform.widget.Widget.handle_error() method in the MyInputWidget class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 from deform.widget import Widget
 from colander import null
 import cgi

 class MyInputWidget(Widget):
     def serialize(self, field, cstruct, readonly=False):
         if cstruct is null:
             cstruct = u''
         quoted = cgi.escape(cstruct, quote='"')
         return ('<input type="text" name="%s" value="%s">' %
                 (field.name, quoted))

     def deserialize(self, field, pstruct):
         if pstruct is null:
             return null
         return pstruct

     def handle_error(self, field, error):
         if field.error is None:
             field.error = error
         for e in error.children:
             for num, subfield in enumerate(field.children):
                 if e.pos == num:
                     subfield.widget.handle_error(subfield, e)

The handle_error method of a widget must:

  • Set the error attribute of the field object it is passed if the error attribute has not already been set.
  • Call the handle_error methods of any subfields which also have errors.

The ability to override handle_error exists purely for advanced tasks, such as presenting all child errors of a field on a parent field. For example:

1
2
3
4
5
6
7
8
 def handle_error(self, field, error):
     msgs = []
     if error.msg:
         field.error = error
     else:
         for e in error.children:
             msgs.append('line %s: %s' % (e.pos+1, e))
         field.error = Invalid(field.schema, '\n'.join(msgs))

This implementation does not attach any errors to field children. Instead it attaches all of the child errors to the field itself for review.

The Template

The template you use to render a widget will receive input from the widget object, including field, which will be the field object represented by the widget. It will usually use the field.name value as the name input element of the primary control in the widget, and the field.oid value as the id element of the primary control in the widget.