How to set css class of a label in a django form declaration?

I'm using django-uniform and to use some uniform features, I'm looking for a way to add css class directly from form declaration (for independents widgets).

(as a bonus, here my reusable read-only home made mixin snippet...)

from django import forms

def _get_cleaner(form, field):
    def clean_field():
        return getattr(form.instance, field, None)
    return clean_field

class UniformROMixin(forms.BaseForm):
    """
    UniformROMixin, inherits to turn some fields read only

      - read_only = list of field names.
    """

    def __init__(self, *args, **kwargs):
        super(UniformROMixin, self).__init__(*args, **kwargs)
        if hasattr(self, "read_only"):
            if self.instance and self.instance.pk:
                for field in self.read_only:
                    self.fields[field].widget.attrs['readonly'] = True
                    self.fields[field].widget.attrs['class'] += "readOnly"
                    # here I would like to set css class of the label
                    # created from the self.fields[field].label string
                    setattr(self, "clean_" + field, _get_cleaner(self, field))

My only way for now is to add a bit of javascript on my generic form template and add classes manualy.

Any brillant idea?


Solution 1:

Widgets have an attrs keyword argument that take a dict which can define attributes for the input element that it renders. Forms also have some attributes you can define to change how Django displays your form. Take the following example:

class MyForm(forms.Form):
    error_css_class = 'error'
    required_css_class = 'required'
    my_field = forms.CharField(max_length=10,
                               widget=forms.TextInput(attrs={'id': 'my_field',
                                                             'class': 'my_class'}))

This works on any Widget class. Unfortunately, there isn't an easy way to change how Django renders labels if you just do {{ field }}. Luckily, you play with the form object a little bit in the template:

<form>
    {% for field in form %}
        <label class="my_class" for="{{ field.auto_id }}">{{ field.label }}</label>
        {{ field }}
    {% endfor %}
    <button type="submit">Submit</button>
</form>

Then again, you can always add arbitrary attributes to the objects you're working with:

# In a view...
form = MyForm()
form.label_classes = ('class_a', 'class_b', )
# Or by hijacking ```__init__```
class MyForm(forms.Form):
    def __init__(self, *args, **kwargs):
        self.my_field = forms.CharField(max_length=10,
                                        widget=forms.TextInput(attrs={'id': 'my_field',
                                                                      'class': 'my_class'}))
        self.my_field.label_classes = ('class_a', 'class_b', )
        super(MyForm, self).__init__(*args, **kwargs)

Render your template with the form in context, and in the template you can do:

<form>
    {% for field in form %}
        <label class="{% for class in field.label_classes %}{{ class }} {% endfor %}"
               for="{{ field.auto_id }}">{{ field.label }}</label>
        {{ field }}
    {% endfor %}
    <button type="submit">Submit</button>
</form>

Whatever suits your fancy.

Solution 2:

I found this snippet which may be a good answer:

How to add css class and "*" to required fields's labels

here an adaptation to my needs (not tested yet, I'll edit the post once done):

from django.utils.html import escape

def readonly_cssclass_adder(bound_field, label_content, label_attrs):
    if 'readonly' in bound_field.field.widget.attrs:
        if 'class' in attrs:
            label_attrs['class'] += " readOnly"
        else:
            label_attrs['class'] = "readOnly"
    return label_content, label_attrs
        

def add_required_label_tag(original_function, tweak_foos=None):
    if not tweak_foos:
        return original_function

    def required_label_tag(self, contents=None, attrs=None):
        contents = contents or escape(self.label)
        if attrs is None:
            attrs = {}
        for foo in tweak_foos:
            contents, attrs = foo(self, contents, attrs)
        return original_function(self, contents, attrs)
    return required_label_tag

def decorate_bound_field():
    from django.forms.forms import BoundField
    BoundField.label_tag = add_required_label_tag(BoundField.label_tag, 
                                           tweak_foos=[readonly_cssclass_adder])

If you have a better solution which don't tweak all the BoundField class I'm still listening.

edit: may be linked to django uniform way to handle required field but it seems to don't call readonly_cssclass_adder. But I found an other and easyer solution, my readOnly widget automatically turned readOnly ctrlHolder

This addition is my favorite response for now:

edit 2: The other way I choose at end was to "override" the uni_form/field.html template which don't call BoundField.label_tag. So I checked here field state.

<label for="{{ field.auto_id }}"{% if field.field.required %}
       class="requiredField{% if field.widget.attrs.readonly %} readOnlyLabel{% endif %}"
       {% else %}{% if field.widget.attrs.readonly %}class="readOnlyLabel"{% endif %}{% endif %}>
    {{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</label>