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.
1def serialize(self, field, cstruct, readonly=False):
2 if cstruct in (null, None):
3 cstruct = ''
4 template = readonly and self.readonly_template or self.template
5 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<span tal:define="name name|field.name;
2 css_class css_class|field.widget.css_class;
3 oid oid|field.oid;
4 style style|field.widget.style;
5 autofocus autofocus|field.autofocus"
6 tal:omit-tag="">
7 <input type="text"
8 name="${name}"
9 value="${cstruct}"
10 data-provide="typeahead"
11 tal:attributes="class string: form-control ${css_class or ''};
12 style style;
13 autofocus autofocus;
14 attributes|field.widget.attributes|{};"
15 id="${oid}"/>
16 <script tal:condition="field.widget.values" type="text/javascript">
17 deform.addCallback(
18 '${field.oid}',
19 function (oid) {
20 $('#' + oid).typeahead(${structure:options});
21 if("${autofocus}" == "autofocus") {
22 $('#' + oid).focus();
23 }
24 }
25 );
26 </script>
27</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:
1import deform
2
3form = deform.Form(someschema)
4resources = form.get_widget_resources()
5js_resources = resources['js']
6css_resources = resources['css']
7js_links = [ 'http://my.static.place/%s' % r for r in js_resources ]
8css_links = [ 'http://my.static.place/%s' % r for r in css_resources ]
9js_tags = ['<script type="text/javascript" src="%s"></script>' % link
10 for link in js_links]
11css_tags = ['<link rel="stylesheet" href="%s"/>' % link
12 for link in css_links]
13tags = js_tags + css_tags
14return {'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.
1registry = deform.widget.ResourceRegistry()
2registry.set_js_resources('requirement', 'ver', 'bar.js', 'baz.js')
3registry.set_css_resources('requirement', 'ver', 'foo.css', 'baz.css')
4
5form = Form(schema, resource_registry=registry)
6resources = form.get_widget_resources()
7js_resources = resources['js']
8css_resources = resources['css']
9js_links = [ 'http://my.static.place/%s' % r for r in js_resources ]
10css_links = [ 'http://my.static.place/%s' % r for r in css_resources ]
11js_tags = ['<script type="text/javascript" src="%s"></script>' % link
12 for link in js_links]
13css_tags = ['<link type="text/css" href="%s"/>' % link
14 for link in css_links]
15tags = js_tags + css_tags
16return {'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:
1registry = deform.widget.ResourceRegistry()
2registry.set_js_resources('requirement', 'ver', 'bar.js', 'baz.js')
3registry.set_css_resources('requirement', 'ver', 'foo.css', 'baz.css')
4Form.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¶
1from deform.widget import Widget
2
3class MyWidget(Widget):
4 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 namejquery.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.
1from deform.widget import Widget
2
3class MyWidget(Widget):
4 requirements = ( {
5 "js": "my:static/path/to/jquery.js",
6 "css": [
7 "my:static/path/to/jquery.css",
8 "my:static:path/to/bootstrap.css"],
9 } )
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 from deform.widget import Widget
2
3 class MyInputWidget(Widget):
4 def serialize(self, field, cstruct=None, readonly=False):
5 # ...
6
7 def deserialize(self, field, pstruct=None):
8 # ...
9
10 def handle_error(self, field, error):
11 # ...
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 from deform.widget import Widget
2 from colander import null
3 import cgi
4
5 class MyInputWidget(Widget):
6 def serialize(self, field, cstruct=None, readonly=False):
7 if cstruct is null:
8 cstruct = u''
9 quoted = cgi.escape(cstruct, quote='"')
10 return ('<input type="text" name="%s" value="%s">' %
11 (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 from deform.widget import Widget
2 from colander import null
3 import cgi
4
5 class MyInputWidget(Widget):
6 def serialize(self, field, cstruct, readonly=False):
7 if cstruct is null:
8 cstruct = u''
9 quoted = cgi.escape(cstruct, quote='"')
10 return ('<input type="text" name="%s" value="%s">' %
11 (field.name, quoted))
12
13 def deserialize(self, field, pstruct):
14 if pstruct is null:
15 return null
16 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 from deform.widget import Widget
2 from colander import null
3 import cgi
4
5 class MyInputWidget(Widget):
6 def serialize(self, field, cstruct, readonly=False):
7 if cstruct is null:
8 cstruct = u''
9 quoted = cgi.escape(cstruct, quote='"')
10 return ('<input type="text" name="%s" value="%s">' %
11 (field.name, quoted))
12
13 def deserialize(self, field, pstruct):
14 if pstruct is null:
15 return null
16 return pstruct
17
18 def handle_error(self, field, error):
19 if field.error is None:
20 field.error = error
21 for e in error.children:
22 for num, subfield in enumerate(field.children):
23 if e.pos == num:
24 subfield.widget.handle_error(subfield, e)
The handle_error
method of a widget must:
Set the
error
attribute of thefield
object it is passed if theerror
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 def handle_error(self, field, error):
2 msgs = []
3 if error.msg:
4 field.error = error
5 else:
6 for e in error.children:
7 msgs.append('line %s: %s' % (e.pos+1, e))
8 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.