Django: How to add dynamic classes to select optio

2019-09-07 11:18发布

问题:

I have a ModelForm that I'm rendering in the template with django-widget-tweaks. I have a ChoiceField select that prints out something like this:

<select>
    <option value="object.id">object.name</option>
</select>

But for Javascript manipulation purposes, I need the following:

<select>
    <option value="object.id" class="object.otherPropery">object.name</option>
</select>

django-widget-tweaks has an add_class function, but it only adds a class to the select box, not the contained options.

I have also tried rolling my own select box:

<select>
    {% for object in form.object_field %}
        <option value="{{ object.id }}" class="{{ object.otherProperty }}">{{ object.name }}</option>
    {% endfor %}
</select>

But when the form does not validate and errors are returned, the option the user had selected previously gets reset back to the default (first) option (this does happen with django-widget-tweaks).

I have also tried overriding the render_option and render_options fields of the Select widget, but the additional properties of the object are not being passed to those functions.

I feel like there's a simple solution for what I'm trying to do. Any help would be much appreciated!

回答1:

I guess you can do something like this (note the 'if' tag):

<select>
    {% with value=form.object_field.value %}
        {% for object in form.object_field %}
            <option 
                value="{{ object.id }}" 
                class="{{ object.otherProperty }}"
                {% if object.id == value %}selected="selected"{% endif %}
            >
                {{ object.name }}
            </option>
       {% endfor %}
    {% endwith %}
</select>

Although the if condition might not result in a true result when it should, if the object.id is a integer because the value would always be a string for a already submitted form (all the values in the POST/GET query data are strings). But if the value comes from the 'initial_data' it would be integer.

For such cases you may update your 'if' tag as follows:

{% if object.id|slugify == value|slugiy %}selected="selected"{% endif %}

Making the final code as follows:

<select>
    {% with value=form.object_field.value %}
        {% for object in form.object_field %}
            <option 
                value="{{ object.id }}" 
                class="{{ object.otherProperty }}"
                {% if object.id|slugify == value|slugify %}selected="selected"{% endif %}
            >
                {{ object.name }}
            </option>
       {% endfor %}
    {% endwith %}
</select>


回答2:

I think there are problems with adding classes or anything else to option elements. I know the applies for CSS for certain, see this other post for example: Can we add class attribute in option element? I think the same might apply to manipulating it with JavaScript.

It might be a little tricky, but maybe in the template make it write out some JavaScript that maps the {{ object.id }} to the {{ object.otherProperty }}, so you have access to the value that way.



回答3:

You can try this.

render_option has been removed from Django 1.11 onwards. This is what I did to achieve this. A little bit of digging and this seems straightforward. Works with Django 2.0+

class CustomSelect(forms.Select):
    def __init__(self, attrs=None, choices=()):
        self.custom_attrs = {}
        super().__init__(attrs, choices)

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        index = str(index) if subindex is None else "%s_%s" % (index, subindex)
        if attrs is None:
            attrs = {}
        option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
        if selected:
            option_attrs.update(self.checked_attribute)
        if 'id' in option_attrs:
            option_attrs['id'] = self.id_for_label(option_attrs['id'], index)

        # setting the attributes here for the option
        if len(self.custom_attrs) > 0:
            if value in self.custom_attrs:
                custom_attr = self.custom_attrs[value]
                for k, v in custom_attr.items():
                    option_attrs.update({k: v})

        return {
            'name': name,
            'value': value,
            'label': label,
            'selected': selected,
            'index': index,
            'attrs': option_attrs,
            'type': self.input_type,
            'template_name': self.option_template_name,
        }


class MyModelChoiceField(ModelChoiceField):

    # custom method to label the option field
    def label_from_instance(self, obj):
        # since the object is accessible here you can set the extra attributes
        if hasattr(obj, 'type'):
            self.widget.custom_attrs.update({obj.pk: {'type': obj.type}})
        return obj.get_display_name()

The form:

class BookingForm(forms.ModelForm):

    customer = MyModelChoiceField(required=True,
                                  queryset=Customer.objects.filter(is_active=True).order_by('name'),
                                  widget=CustomSelect(attrs={'class': 'chosen-select'}))

The output which I needed is as:

  <select name="customer" class="chosen-select" required="" id="id_customer">
      <option value="" selected="">---------</option>
      <option value="242" type="CNT">AEC Transcolutions Private Limited</option>
      <option value="243" type="CNT">BBC FREIGHT CARRIER</option>
      <option value="244" type="CNT">Blue Dart Express Limited</option>