31

I need some way to add a class attribute to the output of the label_tag() method for a forms field.

I see that there is the ability to pass in an attrs dictionary and I have tested it in the shell and I can do something like:

for field in form:
    print field.label_tag(attrs{'class':'Foo'})

I will see the class='Foo' in my output, but I don't see a way to add an attrs argument from the template - in fact, templates are designed specifically against that, no?

Is there a way in my form definition to define the class to be displayed in the label?

In the form, I can do the following to give the inputs a class

self.fields['some_field'].widget.attrs['class'] = 'Foo'

I just need to have it output the class for the <label /> as well.

djvg
  • 11,722
  • 5
  • 72
  • 103
ashchristopher
  • 25,143
  • 18
  • 48
  • 49
  • It looks like Django 4.0 will make life a lot easier, in this respect, with [template based form rendering](https://docs.djangoproject.com/en/dev/releases/4.0/#template-based-form-rendering). – djvg Sep 24 '21 at 19:38

10 Answers10

22

Technique 1

I take issue with another answer's assertion that a filter would be "less elegant." As you can see, it's very elegant indeed.

@register.filter(is_safe=True)
def label_with_classes(value, arg):

    return value.label_tag(attrs={'class': arg})

Using this in a template is just as elegant:

{{ form.my_field|label_with_classes:"class1 class2"}}

Technique 2

Alternatively, one of the more interesting technique I've found is: Adding * to required fields.

You create a decorator for BoundField.label_tag that will call it with attrs set appropriately. Then you monkey patch BoundField so that calling BoundField.label_tag calls the decorated function.

from django.forms.forms import BoundField

def add_control_label(f):
    def control_label_tag(self, contents=None, attrs=None):
        if attrs is None: attrs = {}
        attrs['class'] = 'control-label'
        return f(self, contents, attrs) 
    return control_label_tag

BoundField.label_tag = add_control_label(BoundField.label_tag)    
T. Christiansen
  • 1,036
  • 2
  • 19
  • 34
user240515
  • 3,056
  • 1
  • 27
  • 34
  • 4
    Technique 1 is giving me "AttributeError: 'SafeText' object has no attribute 'label_tag'" with Django 1.6. Could you please check? – Adam Nov 24 '13 at 05:53
  • It is because of a space. Remove the space when you write the filter, before and after | – hlkstuv_23900 Jan 21 '15 at 10:07
  • @AdamSilver Did you find solution for this? – Anuj Mar 04 '16 at 04:12
  • @Anuj Technique 1 with something like {{ form.password|label_with_classes:"col-sm-2 control-label" }} works fine. – Adam Mar 04 '16 at 19:26
  • Using technique 2, when I go to admin to add or edit an object it throws a TypeError: `control_label_tag() got an unexpected keyword argument 'label_suffix'` – Pavel Shlepnev Jan 08 '22 at 10:59
11

How about adding the CSS class to the form field in the forms.py, like:

class MyForm(forms.Form):
    title = forms.CharField(widget=forms.TextInput(attrs={'class': 'foo'}))

then I just do the following in the template:

<label for="id_{{form.title.name}}" class="bar">
    {{ form.title }}
</label>

Of course this can easily be modified to work within a for loop tag in the template.

8

A custom template tag seems to be the solution. A custom filter would also do, although it can be less elegant. But you would need to fall back to custom form rendering in both cases.

If this is a task of high importance; I'd create a Mixin that allows me to annotate the form fields with label classes and supplies form rendering methods using those classes. So that the following code works:

{{ form.as_table_with_label_classes }}

But I'd like to ask; Do you really need a class on the label tag? I mean HTML design-wise. Is it absolutely necessary to add a class in there? Couldn't it be solved with some CSS like:

encapsulating_selector label {
    some-attr: some-value;
}

I sometimes use jQuery for such cases where; it will improve the page if it works, but it won't be a disaster if it doesn't. And keep the HTML source as lean as possible.

Per Lundberg
  • 3,837
  • 1
  • 36
  • 46
muhuk
  • 15,777
  • 9
  • 59
  • 98
  • 41
    `Do you really need a class on the label tag?`, Yes if you're using a framework like bootstrap or foundation. – vikki Nov 04 '13 at 15:21
3

I am agree with answer number one, with css this could be done, but. What is the reason for this to be in django source?

In django.forms.forms.py there's this definition that shows there's code to display attrs in labels:

class BoundField(StrAndUnicode): 
    ...
    def label_tag(self, contents=None, attrs=None):
        contents = u'<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, unicode(contents))

but _html_output calls this function without attrs:

label = bf.label_tag(label) or ''

So it seems that django is partially prepared to do this but actually it does not use it.

djvg
  • 11,722
  • 5
  • 72
  • 103
  • Django does use this in `contrib.admin`. See [AdminField source](https://github.com/django/django/blob/stable/3.2.x/django/contrib/admin/helpers.py#L141). – djvg Sep 22 '21 at 08:24
2

A bit too late but came across a similar problem. Hope this helps you.

class MyForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['myfield1'].widget.attrs.update(
            {'class': 'form-control'})
        self.fields['myfield2'].widget.attrs.update(
            {'class': 'form-control'})

    def as_two_col_layout(self):

        return self._html_output(
            normal_row='<div class="form-group"><span class="col-xs-2">%(label)s</span> <div class="col-xs-10">%(field)s%(help_text)s</div></div>',
            error_row='%s',
            row_ender='</div>',
            help_text_html=' <span class="helptext">%s</span>',
            errors_on_separate_row=True)

    class Meta:

        model = mymodel
        fields = ['myfield1', 'myfield2']
Alessandro Da Rugna
  • 4,571
  • 20
  • 40
  • 64
Tarun Behal
  • 908
  • 6
  • 11
  • I try to use this method, but the label aren't styled. Can you show me your template? I have this in my template: {% for field in form %} {{ field.label_tag }}{{ field }} {% if field.help_text %} {{ field.help_text|safe }} {% endif %} {% endfor %} It seems that the html_output is not applied... – cwhisperer Jan 05 '17 at 15:07
  • or it is only used when using by as_table(), as_ul(), as_p() ? – cwhisperer Jan 05 '17 at 15:18
  • 1
    @cwhisperer in your template youv've to use as form.as_two_col_layout and the code will do the job. – Tarun Behal Jan 09 '17 at 13:14
  • this works fine... how do I say for myfield1 it is a textarea? – cwhisperer Jan 09 '17 at 17:52
1
class CustomBoundField(BoundField):
    def label_tag(self, contents=None, attrs=None):
        if self.field.required:
            attrs = {'class': 'required'}
        return super(CustomBoundField, self).label_tag(contents, attrs)

class ImportViewerForm(forms.Form):
    url = fields.URLField(widget=forms.TextInput(attrs={'class': 'vTextField'}))
    type = fields.ChoiceField(choices=[('o', 'Organisation'), ('p', 'Program')], widget=forms.RadioSelect,
                              help_text='Url contain infornation about this type')
    source = fields.ChoiceField(choices=[('h', 'hodex'), ('s', 'studyfinder')], initial='h', widget=forms.RadioSelect)

    def __getitem__(self, name):
        "Returns a BoundField with the given name."
        try:
            field = self.fields[name]
        except KeyError:
            raise KeyError('Key %r not found in Form' % name)
        return CustomBoundField(self, field, name)

class Media:
    css = {'all': [settings.STATIC_URL + 'admin/css/forms.css']}

You need change method label_tag in BoundField class, and use it in form

user2732686
  • 464
  • 4
  • 6
0
@register.simple_tag
def advanced_label_tag(field):
    """ Return form field label html marked to fill by `*` """
    classes = []
    attrs = {}
    contents = force_unicode(escape(field.label))

    if field.field.required:
        classes.append(u'required')
        contents = force_unicode('%s <span>*</span>'%escape(field.label))

    if classes:
        attrs['class'] = u' '.join(classes)

    return field.label_tag(contents=contents, attrs=attrs)
Alex Isayko
  • 322
  • 2
  • 11
0

we can also use {{field.label}} and {{field.id_for_label}}

<label class="your_class_name" id="{{form.link.id_for_label}}">{{form.link.label}}</label>

Render in HTML as-

<label class="your_class_name" id="id_name">Name</label>
0

NOTE: Hopefully the upcoming Django 4.0 will make this a whole lot easier, as it offers template based form rendering.

Until then:

The OP asks for a way to use BoundField.label_tag() in a form definition.

The answers by user240515 and user2732686 do provide some suggestions for implementation, but they do not provide any rationale. Most other solutions based on custom template tags require us to render the form fields manually, so they do not work if we simply want to use {{ form }}.

Thus, in addition to all those answers, here's an attempt to provide some more background.

About label_tag

Form labels are rendered by the BaseForm.as_table(), as_ul(), or as_p() shortcuts, via the "private" BaseForm._html_output() method, as can be seen in the source.

This is done by calling BoundField.label_tag(), as can be seen here. The label_tag() method takes an attrs argument with additional HTML attributes for the <label> tag.

However, the problem is that BaseForm._html_output() calls label_tag() without attrs, and there is no easy alternative for setting the attrs argument.

How to extend label_tag?

Django's contrib.admin solves this problem by extending the label_tag() method in its AdminField, as becomes clear from the source.

To extend BoundField.label_tag(), we can create a customized BoundField:

class MyBoundField(forms.BoundField):
    def __init__(self, form, field, name, label_attrs=None):
        super().__init__(form, field, name)
        self.label_attrs = label_attrs

    def label_tag(self, contents=None, attrs=None, label_suffix=None):
        if attrs is None:
            attrs = dict()
        attrs.update(self.label_attrs or {})
        return super().label_tag(contents, attrs, label_suffix)

Now we can create a bound field with specific label attributes, but what do we do with it?

How to use a custom bound field?

Bound fields are instantiated using forms.Field.get_bound_field(), so, we could override that method to return our custom bound field:

class MyField(forms.Field):
    # note typically we would use any of django's forms.Field subclasses
    def __init__(self, *args, **kwargs):
        # we could also set label_attrs here, based on the field properties
        self.label_attrs = kwargs.pop('label_attrs', None)
        super().__init__(*args, **kwargs)

    def get_bound_field(self, form, field_name):
        return MyBoundField(
            form=form, field=self, name=field_name, label_attrs=self.label_attrs)

Then we can use the custom field on our Form:

class MyForm(forms.Form):
    some_field = MyField(..., label_attrs={'class': 'my-class'})

But what if we want to do this for all our form fields?

How to use a custom bound field for all form fields?

In the end, Field.get_bound_field() is called in BaseForm.__getitem__() (see source). This means we can get a bound field by calling e.g. my_form['some_field'].

In order to use our custom bound field for all the form's fields, we could either monkey patch field.get_bound_field for all fields in the form, or we could override the form's __getitem__() to ignore get_bound_field() and instead use MyBoundField directly.

Here's an example of an override, which is mostly a copy of the original source, except for the MyBoundField line:

class MyForm(forms.Form):
    label_attrs = {'class': 'my-class'}
    
    def __getitem__(self, name):
        """Return a MyBoundField with the given name."""
        try:
            field = self.fields[name]
        except KeyError:
            ...  # left out for clarity
        if name not in self._bound_fields_cache:
            self._bound_fields_cache[name] = MyBoundField(
                form=self, field=field, name=name, label_attrs=self.label_attrs)
        return self._bound_fields_cache[name]

In the end, this all seems like a lot of trouble for a bit of styling.

djvg
  • 11,722
  • 5
  • 72
  • 103
0

Try django-widget-tweaks

$ pip install django-widget-tweaks

Add it to INSTALLED_APPS in your projects settings.py file:

INSTALLED_APPS += [
'widget_tweaks',
]

Use add_label_class filter:

{% load widget_tweaks %}
{{ form.your_field|add_label_class:"label" }}
 # equal to
<label class="label" for="{{ form.your_field.id_for_label }}">{{ form.your_field.label }}</label>

Read the Document to get more information and functions.

tinyhare
  • 2,271
  • 21
  • 25