How to add attributes to option tags?
Asked Answered
S

6

20

I have to add title attribute to options of the ModelChoiceField. Here is my admin code for that:

class LocModelForm(forms.ModelForm):
        def __init__(self,*args,**kwargs):
            super(LocModelForm,self).__init__(*args,**kwargs)
            self.fields['icons'] = forms.ModelChoiceField(queryset = Photo.objects.filter(galleries__title_slug = "markers"))
            self.fields['icons'].widget.attrs['class'] = 'mydds'
                

        class Meta:
            model = Loc
            widgets = {
                'icons' : forms.Select(attrs={'id':'mydds'}), 
                }
        
        class Media:
            css = {
                "all":("/media/css/dd.css",)
                }
            js=(
                '/media/js/dd.js',
                )

class LocAdmin(admin.ModelAdmin):
    form = LocModelForm

I can add any attribute to select widget, but I don't know how to add attributes to option tags. Any idea?

Silversmith answered 25/6, 2011 at 12:58 Comment(1)
There is a general solution which works for Django 2.+ and allows to add a title and other things in options, see https://mcmap.net/q/455110/-django-form-field-choices-adding-an-attributeAedes
M
20

First of all, don't modify fields in __init__, if you want to override widgets use Meta inner class, if you want to override form fields, declare them like in a normal (non-model) form.

If the Select widget does not do what you want, then simply make your own. Original widget uses render_option method to get HTML representation for a single option — make a subclass, override it, and add whatever you want.

class MySelect(forms.Select):
    def render_option(self, selected_choices, option_value, option_label):
        # look at the original for something to start with
        return u'<option whatever>...</option>'

class LocModelForm(forms.ModelForm):
    icons = forms.ModelChoiceField(
        queryset = Photo.objects.filter(galleries__title_slug = "markers"),
        widget = MySelect(attrs = {'id': 'mydds'})
    )

    class Meta:
        # ...
        # note that if you override the entire field, you don't have to override
        # the widget here
    class Media:
        # ...
Morelock answered 25/6, 2011 at 15:56 Comment(4)
Is there any particular reason why the fields should be modified in the Meta inner class rather than the __init__ method? And would the same reasoning apply to modifying / adding widget attributes for a field?Rb
@Rb In most cases, there is no reason not to override __init__ to set widget.attrs. In the majority of use-cases, such as modifying html attributes on an otherwise default widget not doing so via __init__ override violates DRY. Unfortunately for the OP's case, he will have to define a custom widget since the <option> tag is rendered by Select widget class's render_option method.Oblation
This approach can also be used to extend the SelectMultiple widget. Just subclass SelectMultiple and pass it the custom MySelect widget.Trisyllable
render_option was removed in 1.11 btw docs.djangoproject.com/en/2.1/releases/1.11/…Doublehung
Y
9

I had a similar problem, where I needed to add a custom attribute to each option dynamically. But in Django 2.0, the html rendering was moved into the Widget base class, so modifying render_option no longer works. Here is the solution that worked for me:

from django import forms

class CustomSelect(forms.Select):
    def __init__(self, *args, **kwargs):
        self.src = kwargs.pop('src', {})
        super().__init__(*args, **kwargs)

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        options = super(CustomSelect, self).create_option(name, value, label, selected, index, subindex=None, attrs=None)
        for k, v in self.src.items():
            options['attrs'][k] = v[options['value']]
        return options

class CustomForm(forms.Form):
    def __init__(self, *args, **kwargs):
        src = kwargs.pop('src', {})
        choices = kwargs.pop('choices', ())
        super().__init__(*args, **kwargs)
        if choices:
            self.fields['custom_field'].widget = CustomSelect(attrs={'class': 'some-class'}, src=src, choices=choices)

    custom_field = forms.CharField(max_length=100)

Then in views, render a context with {'form': CustomForm(choices=choices, src=src)} where src is a dictionary like this: {'attr-name': {'option_value': 'attr_value'}}.

Yolanda answered 20/12, 2017 at 10:19 Comment(0)
A
6

Here is a solution if you want to use the instance to set the attribute value.

class IconSelectWidget(forms.Select):
    def create_option(self, name, value, *args, **kwargs):
        option = super().create_option(name, value, *args, **kwargs)
        if value:
            icon = self.choices.queryset.get(pk=value)  # get icon instance
            option['attrs']['title'] = icon.title  # set option attribute
        return option

class LocModelForm(forms.ModelForm):
    icons = forms.ModelChoiceField(
        queryset=Photo.objects.filter(galleries__title_slug='markers'),
        widget=IconSelectWidget
    )
Atmolysis answered 12/3, 2019 at 19:47 Comment(3)
Just what I needed @p14z. I needed to add a bootstrap class which the value was similar. Value being: info. The bootstrap class being: text-info. I didn't need the icon var but a tweek of the option attrs was my saving grace from hours of searching: option['attrs']['class'] = f'text-{value}'Blackamoor
@JamesBellaby if you are working forms with bootstrap I recommend you check out Django Crispy Forms, it will make rendering bootstrap forms so easy.Atmolysis
I'm using v5 Bootstrap which I don't think it supports fully yet. Plus that's the only thing I needed so installing an entire package just for select options was a little overkill in my mind. Thanks for the suggestion though.Blackamoor
D
4

Here's a class I made that inherits from forms.Select (thanks to Cat Plus Plus for getting me started with this). On initialization, provide the option_title_field parameter indicating which field to use for the <option> title attribute.

from django import forms
from django.utils.html import escape

class SelectWithTitle(forms.Select):
    def __init__(self, attrs=None, choices=(), option_title_field=''):
        self.option_title_field = option_title_field
        super(SelectWithTitle, self).__init__(attrs, choices)

    def render_option(self, selected_choices, option_value, option_label, option_title=''):
        print option_title
        option_value = forms.util.force_unicode(option_value)
        if option_value in selected_choices:
            selected_html = u' selected="selected"'
            if not self.allow_multiple_selected:
                # Only allow for a single selection.
                selected_choices.remove(option_value)
        else:
            selected_html = ''
        return u'<option title="%s" value="%s"%s>%s</option>' % (
            escape(option_title), escape(option_value), selected_html,
            forms.util.conditional_escape(forms.util.force_unicode(option_label)))

    def render_options(self, choices, selected_choices):
            # Normalize to strings.
            selected_choices = set(forms.util.force_unicode(v) for v in selected_choices)
            choices = [(c[0], c[1], '') for c in choices]
            more_choices = [(c[0], c[1]) for c in self.choices]
            try:
                option_title_list = [val_list[0] for val_list in self.choices.queryset.values_list(self.option_title_field)]
                if len(more_choices) > len(option_title_list):
                    option_title_list = [''] + option_title_list # pad for empty label field
                more_choices = [(c[0], c[1], option_title_list[more_choices.index(c)]) for c in more_choices]
            except:
                more_choices = [(c[0], c[1], '') for c in more_choices] # couldn't get title values
            output = []
            for option_value, option_label, option_title in chain(more_choices, choices):
                if isinstance(option_label, (list, tuple)):
                    output.append(u'<optgroup label="%s">' % escape(forms.util.force_unicode(option_value)))
                    for option in option_label:
                        output.append(self.render_option(selected_choices, *option, **dict(option_title=option_title)))
                    output.append(u'</optgroup>')
                else: # option_label is just a string
                    output.append(self.render_option(selected_choices, option_value, option_label, option_title))
            return u'\n'.join(output)

class LocModelForm(forms.ModelForm):
    icons = forms.ModelChoiceField(
        queryset = Photo.objects.filter(galleries__title_slug = "markers"),
        widget = SelectWithTitle(option_title_field='FIELD_NAME_HERE')
    )
Deferred answered 5/6, 2012 at 17:42 Comment(0)
C
2

Working with Django 1.11 I discovered another way to this this using the documented APIs. If you override get_context and dig down enough into the structure you'll see the individual option attributes in context['widget']['optgroups'][1][option_idx]['attrs']. For example, in my subclass I have this code:

class SelectWithData(widgets.Select):
    option_data = {}

    def __init__(self, attrs=None, choices=(), option_data={}):
        super(SelectWithData, self).__init__(attrs, choices)
        self.option_data = option_data

    def get_context(self, name, value, attrs):
        context = super(SelectWithData, self).get_context(name, value, attrs)
        for optgroup in context['widget'].get('optgroups', []):
            for option in optgroup[1]:
                for k, v in six.iteritems(self.option_data.get(option['value'], {})):
                    option['attrs']['data-' + escape(k)] = escape(v)
        return context
Cavil answered 10/12, 2019 at 20:29 Comment(0)
D
1

From django 1.11 and above the render_option method was removed. see this link: https://docs.djangoproject.com/en/1.11/releases/1.11/#changes-due-to-the-introduction-of-template-based-widget-rendering

Here is a solution that worked for me different than Kayoz's. I did not adapt the names as in the example but i hope it is still clear. In the model form I overwrite the field:

class MyForm(forms.ModelForm):
    project = ProjectModelChoiceField(label=_('Project'), widget=ProjectSelect())

Then I declare the classes from above and one extra, the iterator:

class ProjectModelChoiceIterator(django.forms.models.ModelChoiceIterator):
    def choice(self, obj):
        # return (self.field.prepare_value(obj), self.field.label_from_instance(obj)) #it used to be like this, but we need the extra context from the object not just the label. 
        return (self.field.prepare_value(obj), obj)

class ProjectModelChoiceField(django.forms.models.ModelChoiceField):
   def _get_choices(self):
       if hasattr(self, '_choices'):
           return self._choices
       return ProjectModelChoiceIterator(self)


class ProjectSelect(django.forms.Select):

    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        context = super(ProjectSelect, self).create_option(name, value, label, selected, index, subindex=None, attrs=None)

        context['attrs']['extra-attribute'] = label.extra_attribute #label is now an object, not just a string.
        return context
Doublehung answered 10/12, 2018 at 15:21 Comment(1)
I could use your solution, but reduced your ProjectModelChoiceField class, where I didn't have to overwrite the _get_choices method, but assigned the iterator property: class ProjectModelChoiceField(..): iterator = ProjectModelChoiceIterator This was also with django version 1.11Callipygian

© 2022 - 2024 — McMap. All rights reserved.