I know that HTML5 input types aren't exactly well supported, yet, so I understand not wanting to have these in web2py for the time being (although, my understanding is that they aren't supposed to break in old browsers, just change to the old 'text' types: see http://diveintohtml5.org/detect.html#modernizr). (Or maybe maybe adding them would break backward compatibility). Having the browser popup a helpful tag and not actually send the form until the input is correct (without resorting to javascript) is nice, however. So this is for anyone else looking to change their default widget types to use <input type='something_not_text'
... and to set the 'required' attribute when the IS_NOT_EMPTY
validator is specified.
Modifying SQLFORM to select new defaults
I modified my sqlhtml.py file (in the gluon directory) to use HTML5's input types (rather than everything defaulting to type='text'). So, I added the types: UrlWidget, TelephoneWidget, EmailWidget, ColorWidget, and RangeWidget. And modified the types: IntegerWidget, DoubleWidget, DecimalWidget, TimeWidget, DateWidget and DatetimeWidget.
In class SQLFORM, I added to the widgets = Storage(dict(
list:
telephone = TelephoneWidget,
url = UrlWidget,
email = EmailWidget,
color = ColorWidget,
range = RangeWidget,
and then further down within the __init__
function, I added elif statements to select the corresponding widget when none is specified:
for fieldname in self.fields:
...
if cond:
...
elif field.type == 'text':
inp = self.widgets.text.widget(field, default)
#Here default widgets are set based on the field type:
#integer, double, decimal, date, time, and datetime
#were set to their respective widget types
elif field.type == 'integer':
inp = self.widgets.integer.widget(field, default)
...
elif field.type == 'datetime':
inp = self.widgets.datetime.widget(field, default)
Changing or Adding Widget definitions
The main change from StringWidget to the new types is simply setting the _type
in the default dict()
object. For example, for the decimal and double widgets (which additionally require the default step size to be changed from the default of 1):
default = dict(
_type = 'number',
_step = 'any', #allow any floating point, otherwise default step size is 1
value = (value!=None and str(value)) or '',
)
The other changes are to change the inheritance from StringWidget to FormWidget, and to set attr =
WidgetName . See the example at the bottom.
For the widget classes, I wanted to be slightly lazy. If I had requires = IS_NOT_EMPTY()
in my table field definition, then I wanted my html input to have the required
attribute. So I added to all my input type widgets:
if hasattr(field, 'requires') and field.requires:
if ("IS_NOT_EMPTY" in str(field.requires)):
default['_required'] = 'true'
Technically the attribute should just be "required"; the "required='true'" is just so something is passed in the dictionary object so I wouldn't have to figure out how to modify the INPUT class as well.
Similarly, if I was using an IS_X_IN_RANGE validator, I wanted the min and max attributes of the input tags to be set. So after deciding if field.requires
has IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, etc. I set min and max to field.requires.minimum
and field.requires.maximum
respectively (its probably bad form to grab these values directly, but I was trying to avoid changing other files). So, for example:
if field.requires.minimum != None:
default['_min'] = str(field.requires.minimum)
Actually field.requires could be a list, in which case field.requires.minimum would break, so (grabbing from the formatter function inside dal.py which also needs to step through the validators) field.requires is turned into a list if it isn't one, then the list is iterated over.
So finally, as an example, here is my modified DatetimeWidget (which is marginally more complicated because html5 spec for datetime wants format=''%Y-%m-%dT%H:%MZ").
class DatetimeWidget(FormWidget):
@staticmethod
def widget(field, value, **attributes):
"""
generates an INPUT datetime tag.
see also: :meth:`FormWidget.widget`
"""
default = dict(
_type = 'datetime',
value = (value!=None and str(value)) or '',
)
if hasattr(field, 'requires') and field.requires:
if ("IS_NOT_EMPTY" in str(field.requires)):
default['_required'] = 'true'
if not isinstance(field.requires, (list, tuple)):
requires = [field.requires]
elif isinstance(field.requires, tuple):
requires = list(field.requires)
else:
requires = copy.copy(field.requires)
requires.reverse()
for item in requires:
if ("IS_DATETIME_IN_RANGE" in str(item)):
#html5 wants yyyy-mm-ddThh:mm:ssZ
#or yyyy-mm-ddThh:mm:ss-TimeZoneOffset
if item.minimum != None:
default['_min'] = str(item.minimum.date())+'T'\
+str(field.requires.minimum.time())+'Z'
if item.maximum != None:
default['_max'] = str(item.maximum)+'T'\
+str(item.maximum.time())+'Z'
attr = DatetimeWidget._attributes(field, default, **attributes)
return INPUT(**attr)
Unfortunately, this still fails to set the min and max attributes if the validators are nested (as in IS_EMPTY_OR(IS_DATE_IN_RANGE(...))
). So I probably will have to add a function to some of the validators, that can return the max and min when validators get nested
Also there is probably some way to do something smarter with inheritance; most of the new types were: copy-paste, change _dict
, change attr =
Comments (0)