Introduction
Following on from my article An Alternative ToscaWidgets Setup with Mako I'll show you how to create your own forms and your own fields and how to use different templates for each.
 | I'm no expert on this so what is written here is the result of my own trial and error. I'm hoping someone else will be able to come along and fill in the gaps. |
File Layout
I like to keep my widgets separate from the rest of my Pylons code so I create a new directory called forms and add a new _init.py file to it. Under the forms directory create validators and widgets directories and add init_.py files to each.
Forms
We are going to edit forms/widgets/_init_.py. First of all we want to create our own form type we do this in two stages. First we create a mixin class for any extra attributes we need and then we create the form itself:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | from toscawidgets.widgets.forms.fields import Form
class CustomFormMixin:
"""
Mix-in class for containers that use a custom method to render their fields
"""
params = ["custom_attrs"]
custom_attrs = {}
class CustomForm(Form, CustomFormMixin):
"""
A form that renders it's fields in a custom way
"""
template = "mako:/widgets/custom_form.mako"
|
Our new form has the following params:
1
2
3
4
5
6 | action = '' # The url where the form's contents should be submitted"
method = 'post' # "The HTTP request method to be used"
submit_text = "Submit" # Text that should appear in the auto-generated Submit button.
# If None then no submit button will be autogenerated.
custom_attrs = {} # The custom_attrs dictionary from the mixin we created
# used to set and custom form attributes we might need
|
Of course forms take all the usual widget options too including:
1
2
3
4 | include_dynamic_js_calls = False
css = []
javascript = []
validator = None
|
 | I don't understand attrs vs params and what exactly the d object is for and its spec |
We now need to create /templates/widgets/custom_form.mako so that the errors, help text and labels are all displayed correctly.
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 | <form
id="${context.get('id')}"
name="${name}"
action="${action}"
method="${method}"
class="${css_class}"
% for k, v in attrs.items():
% if v is not None:
${k}="${v}"
% endif
% endfor
>
<div>
% for field in ihidden_fields:
<div>
<%
error=error_for(field)
%>
${field.display(value_for(field), **args_for(field))}
% if show_children_errors and error and not field.show_error:
<span class="fielderror">${error}</span>
% endif
</div>
% endfor
</div>
<table border="0" cellspacing="0" cellpadding="2"
% for k, v in custom_attrs.items():
% if v is not None:
${k}="${v}"
% endif
% endfor
>
% for i, field in enumerate(ifields):
<%
required = [None,' required'][int(field.is_required)]
error = error_for(field)
label_text = field.label_text
help_text = field.help_text
%>
<tr class="${i%2 and 'odd' or 'even'}">
<th>
% if label_text:
<label
id="${field.id}.label"
for="${field.id}"
class="fieldlabel${required}"
>${label_text}</label>
% endif
</th>
<td>
${field.display(value_for(field), **args_for(field))}
% if help_text:
<span class="fieldhelp">${help_text}</span>
% endif
% if show_children_errors and error and not field.show_error:
<span class="fielderror">${error}</span>
% endif
</td>
</tr>
% endfor
</table>
</form>
|
You can now create define form object like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | from formencode.validators import *
from formencode.schema import Schema
from toscawidgets.api import WidgetsList, CSSSource, JSSource
from toscawidgets.js import js_function
from toscawidgets.widgets.forms import *
class MyForm(CustomForm):
class fields(WidgetsList):
id = HiddenField(
default="I'm hidden!"
)
name = TextField(
validator = UnicodeString(not_empty=True),
default = "Your name here"
)
age = SingleSelectField(
validator = Int,
options = range(100)
)
|
You can then use it in your controller like this:
1 | form = MyForm(name='test', submit_text='Submit', method='post', class_='test', custom_attrs={"class":"form"})
|
 |
The code that this generates currently starts like this:
1
2
3
4
5
6
7 | <form
id="None"
name="None"
action="/calculator/save"
method="post"
class="myform"
>
|
Note how the name and class attributes are incorrect. This is a bug. |
Now that we have created a custom form layout, lets have a look at creating custom fields.
Fields
All fields have the following attributes which are specified as params:
1
2
3
4
5
6
7
8 | show_error = False # Should the field display it's own errors? Defaults to
# False because normally they're displayed by the container widget
disabled = None # Should the field be disbaled on render and it's input ignored by the validator? UNIMPLEMENTED
attrs = {} # Extra attributes for the outermost DOM node
help_text # Description of the field to aid the user
label_text # The text that should label this field
style # Style properties for the field. It's recommended to use "
# css classes and stylesheets instead of this parameter")
|
Creating custom fields is quite similar to creating custom forms.
1
2 | class CustomField(FormField):
template = "mako:/widgets/custom_field.mako"
|
Make this template file like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | <input
% if context.get('type'):
type="${context.get('type')}"
% endif
name="${name}"
class="${css_class}"
id="${context.get('id')}"
value="${value}"
% for k, v in attrs.items():
% if v is not None:
${k}="${v}"
% endif
% endfor
/>
|
 | We had to use the context.get() approach for id and type to ensure we didn't accidentally get the Python functions of the same name instead. |
You can then use your field in the MyForm class we created earlier:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | class MyForm(CustomForm):
class fields(WidgetsList):
id = HiddenField(
default="I'm hidden!"
)
name = TextField(
validator = UnicodeString(not_empty=True),
default = "Your name here"
)
age = SingleSelectField(
validator = Int,
options = range(100)
)
custom = CustomField(
validator = Int,
default = '10',
label = 'Custom field',
)
|
ToscaWidgets forms, fields and other widgets are designed to be instantiated once and then executed lots of times. This poses a problem if say the options to choose from are read from a database and are subject to change. Luckily the developers included a method called update_params() which gets called each time the field is displayed and can be used to change the values each time. Here's how you might use it:
1
2
3
4
5
6
7
8
9
10 | def get_new_value():
# Imagine this fetches the data from the database
return "New value"
class CustomField(FormField):
template = "mako:/widgets/custom_field.mako"
def update_params(self,d):
super(FormField,self).update_params(d)
d['value'] = get_new_value()
|
Obviously it isn't too useful to keep setting the value, but you might want to use this to update options in a SelectionField for example.
CSS
If you look at the form template you'll notice we set classes for label.required and fielderror. We should add some CSS so that any time the form is displayed the appropriate CSS styles are defined too. We do this by creating a CSSSource object and associating it with the form:
1
2
3
4
5
6
7
8
9
10
11
12
13 | css = CSSSource("""
label.required, .fielderror {
font-weight: bold;
color: red;
};
""")
class CustomForm(Form, CustomMixin):
"""
A form that renders it's fields in a custom way
"""
template = "mako:/twForms/custom_form.mako"
css = [css]
|
After you've made this change if you view the source of the generated page you'll see the CSS was automatically added to the head section. You can add more CSSSource entries by adding them to the list assigned to the css attribute.
You can also specify CSS files rather than defining the source. You can do this with a CSSLink class which is used in the same way but generates a link to the source file instead of the source itself.
JavaScript
You can add JavaScript in a similar way to the methods described for CSS but using the JSSource and JSLink classes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | js = JSSource("""
var focus_element = function (elem) {
var elem = document.getElementById(elem);
elem.focus(); elem.select();
};
""",
)
class CustomForm(Form, CustomMixin):
"""
A form that renders it's fields in a custom way
"""
template = "mako:/twForms/custom_form.mako"
css = [css]
javascript = [js]
|
This is already quite useful because you could now hand code any necessary tools to use this javascript into the custom form template we created earlier but ToscaWidgets has a trick up its sleeve to make things even easier. First of all we need to create Python function objects which correspond to the JavaScript ones:
1
2 | alert = js_function('alert')
focus_element = js_function('focus_element')
|
Since alert is a global JavaScript function anyway we didn't need to include a definition for it in the JSScource element created earlier.
Now we can do something quite clever:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | class CustomForm(Form, CustomMixin):
"""
A form that renders it's fields in a custom way
"""
template = "mako:/twForms/custom_form.mako"
css = [css]
javascript = [js]
include_dynamic_js_calls = True
def update_params(self, d):
super(Form, self).update_params(d)
# Focus and select the 'name' field on the form
# The adapter we just wrote lets us pass formfields as parameters and
# the right thing will be done.
if not d.error:
self.add_call(focus_element(d.c.name))
else:
self.add_call(
alert('The form contains invalid data\n%s'% unicode(d.error))
)
|
By overriding the form's update_params() method we can use Python code to add JavaScript calls to our form and ToscaWidgets automatically generates the code to take care of it. Note that we also had to set include_dynamic_js_calls to True for the JS function code to work.
Fieldsets
- XXX Are fieldsets really the best way to create compound fields too or is there a better way which should be discussed separately?
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 | class CustomFieldSetMixin:
"""
Mix-in class for containers that use a custom method to render their fields
"""
params = ["custom_attrs"]
custom_attrs = {}
class CustomFieldSet(FieldSet, CustomFieldSetMixin):
"""
A fieldset that renders it's fields in a custom manner
"""
template = "mako:/twForms/custom_fieldset.mako"
class FilteringSchema(Schema):
filter_extra_fields = True
allow_extra_fields = True
class MyFieldSet(CustomFieldSet):
class fields(WidgetsList):
street = TextField(validator=UnicodeString)
number = TextField(validator=Int, size=4)
zip_code = TextField(validator=PostalCode)
state = TextField(default='NY',validator=StateProvince)
validator = FilteringSchema
class MyForm(CustomForm):
class fields(WidgetsList):
id = HiddenField(
default="I'm hidden!"
)
name = TextField(
validator = UnicodeString(not_empty=True),
default = "Your name here"
)
age = SingleSelectField(
validator = Int,
options = range(100)
)
custom = CustomField(
validator = Int,
default = '10',
label = 'Custom field',
)
numbers = FormFieldRepeater(
label_text = "Test",
widget = TextField(
show_error=True,
validator = Int(not_empty=True),
),
repetitions = 2,
max_repetitions = 5
)
fields = MyFieldSet()
|
Can specify a legend.
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 | <fieldset
id="${id}"
class="${css_class}"
% for k, v in attrs.items():
% if v is not None:
${k}="${v}"
% endif
% endfor
>
% if legend:
<legend>${legend}</legend>
% endif
% if context.kwargs.get("error") and context.kwargs.get("show_error"):
<div class="fielderror">${error}</div>
% endif
<div>
% for field in ihidden_fields:
<% error=error_for(field) %>
<div>
${field.display(value_for(field), **args_for(field))}
% if show_children_errors and error and not field.show_error:
<span class="fielderror">${error}</span>
% endif
</div>
% endfor
</div>
<ul class="field_list"
% |