[Django]-"Disabled" option for choiceField – Django

26👍

Django’s form widgets offer a way to pass a list of attributes that should be rendered on the <option> tag:

my_choices = ( ('one', 'One'), ('two', 'Two'))
class MyForm(forms.Form):
    some_field = forms.ChoiceField(choices=my_choices, 
                                   widget=forms.Select(attrs={'disabled':'disabled'}))

Unfortunately, this won’t work for you because the attribute will be applied to EVERY option tag that is rendered. Django has no way to automatically know which should be enabled and which should be disabled.

In your case, I recommend writing a custom widget. It’s pretty easy to do, and you don’t have that much custom logic to apply. The docs on this are here. In short though:

  • subclass forms.Select, which is the default select renderer
  • in your subclass, implement the render(self, name, value, attrs) method. Use your custom logic to determine if the value qualifies as needing to be disabled. Have a look at the very short implementation of render in django/forms/widgets.py if you need inspriation.

Then, define your form field to use your custom widget:

class MyForm(forms.Form):
    some_field = forms.ChoiceField(choices=my_choices, 
                                   widget=MyWidget)

13👍

You can create Choices as mentioned by Bryan like this. In the below options Root 1, Root 2 are automatically disabled and they will look like Group Options

CHOICES = (
  ('-- Root 1--', 
      (
        ('ELT1', 'ELT1'),
        ('ELT2', 'ELT2'),
        ('ELT3', 'ELT3'),
      )
   ),
  ('-- Root 2--', 
      (
        ('ELT3', 'ELT3'),
        ('ELT4', 'ELT4'),
      )
  ),
)

The above option will display like this. In the below image, Root 1 and Root 2 are not selectable.

enter image description here

Hope this will clear your problem

-Vikram

👤vkrams

8👍

field_choices = (
        ('','Make choice'),
        (1,'first'),
        (2,'second'),
        (3,'third')
    )

from django.forms import Select

class Select(Select):
    def create_option(self, *args,**kwargs):
        option = super().create_option(*args,**kwargs)
        if not option.get('value'):
            option['attrs']['disabled'] = 'disabled'

        if option.get('value') == 2:
            option['attrs']['disabled'] = 'disabled'

        return option

6👍

This might be a late answer but this is a simplified version that can be modified using the form instance.

You can either pass a list of values to be disabled i.e

def __init__(self, disabled_choices, *args, **kwargs):
        self.disabled_choices = disabled_choices

OR

from django.forms import Select


class SelectWidget(Select):
    """
    Subclass of Django's select widget that allows disabling options.
    """
    def __init__(self, *args, **kwargs):
        self._disabled_choices = []
        super(SelectWidget, self).__init__(*args, **kwargs)

    @property
    def disabled_choices(self):
        return self._disabled_choices

    @disabled_choices.setter
    def disabled_choices(self, other):
        self._disabled_choices = other

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        option_dict = super(SelectWidget, self).create_option(
            name, value, label, selected, index, subindex=subindex, attrs=attrs
        )
        if value in self.disabled_choices:
            option_dict['attrs']['disabled'] = 'disabled'
        return option_dict

To disabled an option based on a condition i.e user isn’t a superuser.

class MyForm(forms.Form):
    status = forms.ChoiceField(required=True, widget=SelectWidget, choices=(('on', 'On'), ('off', 'Off')))

    def __init__(self, request, *args, **kwargs):
        super().__init__(*args, **kwargs)

        if not request.user.is_superuser:
            self.fields['status'].widget.disabled_choices = ['off']

4👍

It seems django 1.1 will allow “optgroup”: Django documentation

class MyForm(forms.Form):
    some_field = forms.ChoiceField(choices=[
        ('Audio', (
                ('vinyl', 'Vinyl'),
                ('cd', 'CD'),
            )
        ),
        ('Video', (
                ('vhs', 'VHS Tape'),
                ('dvd', 'DVD'),
            )
        ),
        ('unknown', 'Unknown'),
    ])

This imho is a must have.

👤Pierre

4👍

Simple questions sometimes have complex answers in Django. I spent a lot of time getting this to work well. Combining Jarrett’s overview with an important note from jnns about render_option and some help from #django on freenode I have a well-working complete example solution:

First, this example assumes a normally defined choices-type CharField in a model I call Rule. I subclass my own TimeStampedModel but you can use a models.Model:

class Rule(TimeStampedModel):

    ...

    # Rule Type
    SHORT_TERM_RULE = 'ST'
    MAX_SIGHTINGS_PER_PERIOD_RULE = "MA"
    WHITE_LIST_RULE = "WL"
    BLACK_LIST_RULE = "BL"
    RULE_CHOICES = (
        (SHORT_TERM_RULE, 'Short Term Rule'),
        (MAX_SIGHTINGS_PER_PERIOD_RULE, 'Max Sightings Per Period Rule'),
        (WHITE_LIST_RULE, 'White List Rule'),
        (BLACK_LIST_RULE, 'Black List Rule'),
    )
    rule_type = models.CharField(
        max_length=2,
        choices=RULE_CHOICES,
        default=SHORT_TERM_RULE,
    )

    ...

In forms.py, define the widget subclassing Select that accepts disabled_choices. It has a custom render_option() that adds disabled to the html output of option tags if their choice is included on the passed in disabled_choices list. Note, I left most of the render_option() code from Select as-is:

class MySelect(Select):
    def __init__(self, attrs=None, choices=(), disabled_choices=()):
        super(MySelect, self).__init__(attrs, choices=choices)
        self.disabled_choices = disabled_choices

    def render_option(self, selected_choices, option_value, option_label):
        if option_value is None:
            option_value = ''
        option_value = force_text(option_value)
        if option_value in selected_choices:
            selected_html = mark_safe(' selected="selected"')
            if not self.allow_multiple_selected:
                selected_choices.remove(option_value)
        else:
            selected_html = ''
        for key, value in self.disabled_choices:
            if option_value in key:
                return format_html('<option disabled value="{}"{}>{}</option>', option_value, selected_html,
                                   force_text(option_label))
        return format_html('<option value="{}"{}>{}</option>', option_value, selected_html, force_text(option_label))

Then, in defining the form subclassing ModelForm, check for a passed-in disabled_choices list and initialize the field accordingly. In this example, I also sneak in a default choice.

class RuleChoiceForm(ModelForm):
    class Meta:
        model = Rule
        fields = ['rule_type']

    # Add a default choice to the list defined in the Rule model
    default_choice = ('DF', 'Choose a Rule Type...')
    choices = list(Rule.RULE_CHOICES)
    choices.insert(0, default_choice)
    choices = tuple(choices)

    rule_type = forms.ChoiceField(widget=MySelect(attrs={'class': 'form-control'}, disabled_choices=[]),
                                  choices=choices)

    def __init__(self, *args, disabled_choices=None, **kwargs):
        super(RuleChoiceForm, self).__init__(*args, **kwargs)
        if disabled_choices:
            self.fields['rule_type'].widget.disabled_choices = disabled_choices

Then in your view, define disabled_choices as a list, appending choices from your _CHOICES var in your model and pass it into the form instantiation. In my logic, I use list comprehension of RULE_CHOICES to get the tuple of the choice I want to disable. Though there may be a simpler way, please feel free to post suggestions to simplify or improve this answer.

    disabled_choices = []
    # Logic disabling choices
    if Rule.objects.filter(route=route, rule_type=Rule.SHORT_TERM_RULE).exists():
        disabled_choices.append([item for item in Rule.RULE_CHOICES if Rule.SHORT_TERM_RULE in item][0])
        disabled_choices.append([item for item in Rule.RULE_CHOICES if Rule.MAX_SIGHTINGS_PER_PERIOD_RULE in item][0])

    rule_choice_form = RuleChoiceForm(disabled_choices=disabled_choices)
👤Rob

2👍

Are you trying to create a menu in which the list items are separated into categories, and you don’t want the categories themselves to be selectable?

If so, you can achieve this by having your template render the field using tags, e.g.

<select name="my_field" id="id_my_field">
<optgroup label="-- Root 1 entry --">
    <option value="1">Elt 1</option>
    <option value="2">Elt 2</option>
    <option value="3">Elt 3</option>
</optgroup>
<optgroup label="--- Root 2 entry ---">
    <option value="4">Elt 4</option>
    <option value="5">Elt 5</option>
</optgroup>
</select>
👤bryan

-1👍

I think it’s better to do this using JavaScript.

Add the following line of code to the HTML page that containts your select element:

    <script>
        document.querySelector("select").options[0].disabled = true;
    </script>

And if you want to disable an option in multiple select elements in your page you can do the following:

    <script>
        document.querySelectorAll('select').forEach(function(element) {
            element.options[0].disabled = true;
        });
    </script>

Leave a comment