Customizing Django forms with RadioSelect widget

18,853

Solution 1

Since it doesn't seem to be a good way to do this I chose to rearrange the generated code using jQuery.

// First remove the ul and li tags
$('.radio ul').contents().unwrap();
$('.radio li').contents().unwrap();
// Then move the input to outside of the label
$('.radio > label > input').each(function() {
    $(this).parent().before(this);
});
// Then apply the jQuery UI buttonset
$( ".radio" ).buttonset();

This made it go from:

<ul>
    <li><label for="id_notify_new_friends_0"><input type="radio" id="id_notify_new_friends_0" value="0" name="notify_new_friends" /> Immediately</label></li>
    <li><label for="id_notify_new_friends_1"><input type="radio" id="id_notify_new_friends_1" value="1" name="notify_new_friends" /> Never</label></li>
</ul>

to:

<input type="radio" id="id_notify_new_friends_0" value="0" name="notify_new_friends" /><label for="id_notify_new_friends_0"> Immediately</label></li>
<input type="radio" id="id_notify_new_friends_1" value="1" name="notify_new_friends" /><label for="id_notify_new_friends_1"> Never</label></li>

and my jQuery UI styling works fine.

Solution 2

In my code I discovered that changing the widget from

forms.RadioSelect

to

forms.RadioSelect(attrs={'id': 'value'})

magically causes the resulting tag value to include the id attribute with the index of the item appended. If you use

{% for radio in form.foo %}
    <li>
        {{ radio }}
    </li>
{% endfor %}

in the form you get a label wrapped around an input. If you want the more conventional input followed by label, you need to do this:

{% for radio in form.value %}
    <li>
        {{ radio.tag }}
        <label for="value_{{ forloop.counter0 }}">{{ radio.choice_label }}</label>
    </li>
{% endfor %}

Solution 3

Unfortunately this is more complicated than it should be, it seems you need to override at least 2 classes: RadioRenderer and RadioInput. The following should help you get started but you might need to tweak it a little.

First create a custom radio button input widget. The only purpose of us overriding the render method is to get rid of annoying structure Django enforces (<label><input /></label>) where instead we want ours (<label /><input />):

class CustomRadioInput(RadioInput):
    def render(self, name=None, value=None, attrs=None, choices=()):
        name = name or self.name
        value = value or self.value
        attrs = attrs or self.attrs
        if 'id' in self.attrs:
            label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
        else:
            label_for = ''
            choice_label = conditional_escape(force_unicode(self.choice_label))
            return mark_safe(u'%s<label%s>%s</label>' % (self.tag(), label_for, choice_label))

Now we need to override RadioRenderer in order to:

  1. Force it to use our custom radio input widget
  2. Remove <li> wraping every single input field and <ul> wrapping all input fields:

Something along these lines should do:

class RadioCustomRenderer(RadioFieldRenderer):
    def __iter__(self):
        for i, choice in enumerate(self.choices):
            yield CustomRadioInput(self.name, self.value, self.attrs.copy(), choice, i)

    def __getitem__(self, idx):
        choice = self.choices[idx]
        return CustomRadioInput(self.name, self.value, self.attrs.copy(), choice, idx)

    def render(self):
        return mark_safe(u'%s' % u'\n'.join([u'%s' % force_unicode(w) for w in self]))

Finally instruct Django to use custom renderer

notify_new_friends = forms.ChoiceField(label='Notify when new friends join', widget=forms.RadioSelect(renderer=RadioCustomRenderer), choices=NOTIFICATION_CHOICES)

Please bear in mind: This now outputs radio buttons together with encompassing <td> hence you need to build a table around it in your template, something along these lines:

<table>
  <tr>
    <td><label for="{{field.auto_id}}">{{field.label}}</label></td>
    <td>{{ field.errors }} {{field}}</td>
  </tr>
</table>

Solution 4

If anyone stumble upon this problem and just want to render the radio button without ul: they should follow this link.

https://docs.djangoproject.com/en/3.1/ref/forms/widgets/#selector-widgets

Example below.

{% for radio in myform.beatles %}
    <div class="myradio">
    {{ radio }}
    </div>
{% endfor %}
Share:
18,853
olofom
Author by

olofom

Updated on June 13, 2022

Comments

  • olofom
    olofom almost 2 years

    So I'm using jQuery UI to skin the radio buttons but I can't get Django to render my form the way it has to be done.

    I need to have this structure:

    <table>
        <tr>
            <td><label for="notify_new_friends">Notify when new friends join</label></td>
            <td class="radio">
                <input type="radio" name="notify_new_friends" id="notify_new_friends_immediately" value="1" checked="checked"/><label for="notify_new_friends_immediately">Immediately</label>
                <input type="radio" name="notify_new_friends" id="notify_new_friends_never" value="0"/><label for="notify_new_friends_never">Never</label>
            </td>
        </tr>
    </table>
    

    So to summarize that I need the radio buttons within a class (radio) where they have an input and a label for.

    When I render the form in my template with {{ profile_form.notify_new_friends }} I get the following:

    <ul>
        <li><label for="id_notify_new_friends_0"><input type="radio" id="id_notify_new_friends_0" value="0" name="notify_new_friends" /> Immediately</label></li>
        <li><label for="id_notify_new_friends_1"><input type="radio" id="id_notify_new_friends_1" value="1" name="notify_new_friends" /> Never</label></li>
    </ul>
    

    Which is exactly what I want except for the list-part. So I tried looping over it which gives me the labels formatted differently:

    {% for item in profile_form.notify_new_friends %}
        {{ item }}
    {% endfor %}
    

    which gives me:

    <label><input type="radio" name="notify_new_friends" value="0" /> Immediately</label>
    <label><input type="radio" name="notify_new_friends" value="1" /> Never</label>
    

    So the problem here is that it stops using label for and starts using just label to wrapp it all with.

    I also tried doing something like this, but then the label and label_tag don't render anything.

    {{ profile_form.notify_new_friends.0 }}
    {{ profile_form.notify_new_friends.0.label_tag }}
    {{ profile_form.notify_new_friends.0.label }}
    

    So does anyone know how I can render this properly!?

    FYI, this is my forms.py:

    self.fields['notify_new_friends'] = forms.ChoiceField(label='Notify when new friends join', widget=forms.RadioSelect, choices=NOTIFICATION_CHOICES)