Search code examples
pythonhtmldjangodjango-templatesdjango-template-filters

Adding a conditional class to a Django field's label


I'm trying to refactor a Django template which renders fields manually (cf. https://docs.djangoproject.com/en/2.0/topics/forms/#rendering-fields-manually). The labels are generated as follows:

  <label for="{{ field.id_for_label }}"
      class="{% if field.value %}active{% endif %} {% if field.errors %}invalid{% endif %}">
  </label>

where the field is looped over using {% for field in form %} ... {% endfor %}.

I'm trying to refactor this by writing a custom filter (cf. https://docs.djangoproject.com/en/2.0/howto/custom-template-tags/#writing-custom-template-filters). So far I've come up with the following. In the templatetags directory, I've added a label_with_classes.py which reads

from django import template

register = template.Library()

@register.filter(is_safe=True)
def label_with_classes(value, arg):
    return value.label_tag(attrs={'class': arg})

which I use to replace the HTML above with

  {{ field|label_with_classes:"active"}}

The problem is that this doesn't actually do what the original template does; it always labels it with the class "active" and doesn't implement the conditional logic.

My question: is implementing this logic possible using a filter? What does the value input argument to the filter function actually represent, is it the field.value (as its name suggests) or the field itself?


Solution

  • By dropping into the debugger while the development server was running and refreshing the page, I found that the value is actually an instance of a BoundField, which has a value() method and an errors attribute:

    > /Users/kurtpeek/Documents/Dev/lucy/lucy-web/dashboard/templatetags/label_with_classes.py(8)label_with_classes()
          7     import ipdb; ipdb.set_trace()
    ----> 8     return value.label_tag(attrs={'class': arg})
          9 
    
    ipdb> value
    <django.forms.boundfield.BoundField object at 0x113957eb8>
    ipdb> dir(value)
    ['__bool__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__html__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'as_hidden', 'as_text', 'as_textarea', 'as_widget', 'auto_id', 'build_widget_attrs', 'css_classes', 'data', 'errors', 'field', 'form', 'help_text', 'html_initial_id', 'html_initial_name', 'html_name', 'id_for_label', 'initial', 'is_hidden', 'label', 'label_tag', 'name', 'subwidgets', 'value']
    ipdb> value.errors
    []
    ipdb> value.value
    <bound method BoundField.value of <django.forms.boundfield.BoundField object at 0x113957eb8>>
    ipdb> value.value()
    4
    

    I was a bit confused by the use of the variable value, and have renamed the dummy variable bound_field instead.

    Here is how I implemented the custom filter implementing the conditional classes (in templatetags/label_with_classes.py):

    from django import template
    
    register = template.Library()
    
    
    @register.filter(is_safe=True)
    def label_with_classes(bound_field):
        classes = f"{'active' if bound_field.value() else ''} {'invalid' if bound_field.errors else ''}"
        return bound_field.label_tag(attrs={'class': classes.strip()})
    

    after which the <label> element can be replaced in the template by

    {% load label_with_classes %}
    
    {% for field in form %}
      {{ field|label_with_classes }}
    {% endfor %}