Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customizing Django forms with RadioSelect widget

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)
like image 477
olofom Avatar asked Jul 19 '12 07:07

olofom


People also ask

What a widget is Django how can you use it HTML input elements?

A widget is Django's representation of an HTML input element. The widget handles the rendering of the HTML, and the extraction of data from a GET/POST dictionary that corresponds to the widget. The HTML generated by the built-in widgets uses HTML5 syntax, targeting <! DOCTYPE html> .

What is the significance of introducing custom widgets in Django?

The widget in Django will handle the rendering of the HTML elements and extract data from a POST/GET dictionary that corresponds to the same widget. Whenever we specify a field on a Django form, Django uses some default widget appropriate to the data type to be displayed.

How do forms get rendered using Django templates?

Working with form templates. All you need to do to get your form into a template is to place the form instance into the template context. So if your form is called form in the context, {{ form }} will render its <label> and <input> elements appropriately.


4 Answers

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 %}
like image 154
pdc Avatar answered Oct 16 '22 21:10

pdc


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>
like image 38
tnajdek Avatar answered Oct 16 '22 19:10

tnajdek


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 %}
like image 23
Martins Avatar answered Oct 16 '22 19:10

Martins


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.

like image 31
olofom Avatar answered Oct 16 '22 20:10

olofom