corycorycory
corycorycory

Reputation: 1656

How to add data-attribute to django modelform modelchoicefield

I have a django modelform 'Recipe' with a foreignkey field to a model 'Ingredient'.

When rendering the form I get a SELECT list that have an ID matching the ingredients ID and text display equal to the string representation of the field.

However, I want to add a data-attribute to the select list that matches the rendered option from the Ingredient queryset.

For example, lets say this is what is currently being rendered:

<option value="1158">Carrots</option>
<option value="1159">Strawberry</option>
<option value="1160">Onion</option>
<option value="1161">Spinach</option>

But I want to add a data attribute for the related objects:

<option value="1158" data-ingredient-type="vegetable">Carrots</option>
<option value="1159" data-ingredient-type="fruit">Strawberry</option>
<option value="1160" data-ingredient-type="vegetable">Onion</option>
<option value="1161" data-ingredient-type="vegetable">Spinach</option>

Upvotes: 14

Views: 6772

Answers (4)

mglart
mglart

Reputation: 380

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):

class 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:

class RecipeModelForm(ModelForm):
    
    class Meta:
        model = Recipe
        fields = [
            # other fields ...
            'ingredients',
        ]
        
        field_classes = {
            'ingredients': IngredientChoiceField
        }

Upvotes: 9

Tash000
Tash000

Reputation: 51

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
        )

Upvotes: 4

Code-Apprentice
Code-Apprentice

Reputation: 83577

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.

Upvotes: -1

Resley Rodrigues
Resley Rodrigues

Reputation: 2288

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'}),
   ...}

Upvotes: 4

Related Questions