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>
For this reason, Django provides a helper class that lets you create a Form class from a Django model. The generated Form class will have a form field for every model field specified, in the order specified in the fields attribute. Each model field has a corresponding default form field.
When creating a form using Django, form fields are an essential part of creating the Django Form class. There are over 23 built-in field classes with build-in validations and clean () methods. We will cover commonly used form fields along with form widgets to quickly get you on your way to creating forms for any site.
Django will prevent any attempt to save an incomplete model, so if the model does not allow the missing fields to be empty, and does not provide a default value for the missing fields, any attempt to save () a ModelForm with missing fields will fail.
If you wish to connect a Django model to the form, import the model from the models.py file and use the ModelChoiceField () and specify the queryset in the form field. Set the initial to 0 if you wish for the first model object value to be the initial value of the drop-down menu.
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
}
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'}),
...}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With