2008-05-23

Decorating the Render Methods of New-Form Widgets

Perhaps all template masters have already faced the problem of styling HTML input fields of different types. The selectors like input[type=checkbox] and similar in CSS are not supported by IE so people working with templates and CSS obviously need some other way to select and style specific types of input fields.

There are a few ugly ways to do that which violate the DRY principle:

  • Encompassing the form element in the template with HTML tag which has a class representing specific type of the input field.
    For example:
    <span class="form_checkbox">
    {{ form.is_privacy_policy_confirmed }}
    </span>


  • Defining the specific CSS class for each form field widget in the form.
    For example:
    class FormExample(forms.Form):
    is_privacy_policy_confirmed = forms.BooleanField(
    required=True,
    widget=CheckboxInput(attrs={'class': 'form_checkbox'}),
    )


  • Extending all Fields and all Widgets which use HTML input fields and using the extended versions instead of the originals.



I don't like any of them, because they force me or the template formatters to repeat ourselves and make plenty of replacements in our existing forms.

Although "most sane developers consider it a bad idea", I see the Guerrilla patching of the Widget's render method as the nicest solution to solve this problem.

Guerrilla patch is the modification of the runtime code in dynamic languages without changing the original source code.

The method render of the Widget class draws the input field in HTML. As it takes a parameter attrs for additional input field attributes, my idea was to create a decorator which modifies the incoming parameters and adds a CSS class "form_TYPE", where TYPE is the input field type.

With a little help by Dalius Dobravolskas, I succeeded to code a decorator having an optional parameter which defines the CSS class name for the field. If the class name is not defined, the attribute input_type of the Widget class is used for forming the CSS class name (N.B. not all widgets have this attribute).

from django.newforms.widgets import Input, CheckboxInput, RadioSelect, CheckboxSelectMultiple

### adding class="form_*" to all html input fields ###
def add_css_class(css_class=""):
def modify_input_class(function):
_css_class = css_class
def new_function(*args, **kwargs):
arg_names = function.func_code.co_varnames
new_kwargs = dict(zip(arg_names, args))
new_kwargs.update(kwargs)
css_class = _css_class or "form_%s" % getattr(
new_kwargs['self'],
"input_type",
"undefined",
)
self = new_kwargs.pop("self")
attrs = getattr(self, "attrs", None) or {}
if "class" in attrs:
css_classes = attrs["class"].split()
if css_class not in css_classes:
css_classes.append(css_class)
attrs["class"] = " ".join(css_classes)
else:
attrs["class"] = css_class
self.attrs = attrs
return function(self, **new_kwargs)
return new_function
return modify_input_class
Input.render = add_css_class()(Input.render)
CheckboxInput.render = add_css_class("form_checkbox")(CheckboxInput.render)
RadioSelect.render = add_css_class("form_radio")(RadioSelect.render)
CheckboxSelectMultiple.render = add_css_class("form_checkbox")(CheckboxSelectMultiple.render)


To use this code, just place it in some models.py file in your project.

The strange part here was that the variable css_class isn't recognized by the sub-child function new_function directly although the scope of the variable css_class should let it be accessed there. Anyway, the value got easily accessible when I reassigned it to another variable like _css_class in the child function modify_input_class.

The tricky part of this snippet was getting the attrs argument from the decorated function as it was not clear whether it would be passed as a positional or as a named argument. The first three lines of the function new_function collects all the incoming arguments to a dictionary new_kwargs. They can be modified and then passed to the original function to decorate.

Ups. I am late to the studio. So see you next time!

1 comment: