[Solved]-How to add data-attribute to django modelform modelchoicefield

8πŸ‘

One way is using a custom Select widget which allows passing individual attributes in options, through the label part of the widget choices:
(code from this great answer)

class SelectWithOptionAttribute(Select):
   
"""
Use a dict instead of a string for its label. The 'label' key is expected
for the actual label, any other keys will be used as HTML attributes on
the option.
"""

def create_option(self, name, value, label, selected, index, 
                  subindex=None, attrs=None):
    # This allows using strings labels as usual
    if isinstance(label, dict):
        opt_attrs = label.copy()
        label = opt_attrs.pop('label')
    else: 
        opt_attrs = {}
    option_dict = super().create_option(name, value, 
        label, selected, index, subindex=subindex, attrs=attrs)
    for key,val in opt_attrs.items():
        option_dict['attrs'][key] = val
    return option_dict

To populate the individual options override label_from_instance method on a ModelChoiceField subclass(see django docs):

IngredientChoiceField(ModelChoiceField):
"""ChoiceField with puts ingredient-type on <options>"""

# Use our custom widget:
widget = SelectWithOptionAttribute

def label_from_instance(self, obj):
# 'obj' will be an Ingredient
    return {
        # the usual label:
        'label': super().label_from_instance(obj),
        # the new data attribute:
        'data-ingredient-type': obj.type
    }

Finally, simple use this field in a form:

RecipeModelForm(ModelForm):

class Meta:
    model = Recipe
    fields = [
        # other fields ...
        'ingredients',
    ]
    
    field_classes = {
        'ingredients': IngredientChoiceField
    }
πŸ‘€mglart

4πŸ‘

Why not render the fields manually?
It”ll be something like

<select>
  {% for option in form.ingredient.choices %}
     <option value="{{ option.id }}" data-ingredient-type={{ option.type }}>{{ option.name }}</option>
  {% endfor %}
</select>  

Or maybe in you model form class you add the attribute to it, but this must be a string (or probably a function)

widgets = { ...
     'ingredients' = forms.Select(attrs={'data-ingredient-type': 'fruit'}),
   ...}

2πŸ‘

This got a lot easier on the new versions of Django:

class SelectWithAttributeField(forms.Select):
    def create_option(
        self, name, value, label, selected, index, subindex=None, attrs=None
    ):
        option = super().create_option(
            name, value, label, selected, index, subindex, attrs
        )

        if value:
            option["attrs"]["data-ingredient-type"] = value.instance. ingredient
        return option

While at the Recipe model form use it as:

class RecipeForm(forms.ModelForm):

    class Meta:
        model = Recipe
        fields = "__all__"
        widgets = {
            "ingredient": SelectWithAttributeField,
        }
    
    def __init__(self, *args, **kwargs):
        super(RecipeForm, self).__init__(*args, **kwargs)

        self.fields["ingredient"].queryset = Ingredient.objects.filter(
            recipe=recipe
        )
πŸ‘€Tash000

0πŸ‘

My solution was to create a custom widget that overrides create_option():

class IngredientSelect(forms.Select):
    def create_option(
        self, name, value, label, selected, index, subindex=None, attrs=None
    ):
        option = super().create_option(
            name, value, label, selected, index, subindex, attrs
        )
        if value:
            ingredient = models.Ingredient.objects.get(pk=value)
            option['attrs'].update({
                'data-type': ingredient.type
            })
        return option

Then you need to specify the widget to use for the ingredient field in your form:

class RecipeForm(forms.ModelForm):
    class Meta:
        model = models.Recipe
        fields = '__all__'
        widgets = {
            'ingredient': IngredientSelect,
        }

Thanks to In Django form, custom SelectField and SelectMultipleField to pointing me towards this solution.

I’m not entirely satisfied with this solution because it assumes that value is a pk for Ingredient and executes a direct database query to get the Ingredient option. It seems like the model object should be available from the ModelChoiceField, but I was unable to find a way to get it.

Leave a comment