1
0
mirror of https://github.com/django/django.git synced 2025-04-22 00:04:43 +00:00

Fixed #34034 -- Allow setting HTML attributes on Select widget options.

Co-authored-by: Mariana <mmariana.pereira.20@gmail.com>
This commit is contained in:
Kees Hink 2023-06-02 13:45:11 +02:00 committed by Sarah Boyce
parent cf03aa4e94
commit d667c2b894
6 changed files with 133 additions and 4 deletions

View File

@ -500,6 +500,7 @@ class AutocompleteMixin:
self.db = using
self.choices = choices
self.attrs = {} if attrs is None else attrs.copy()
self.option_attrs = {}
self.i18n_name = get_select2_language()
def get_url(self):

View File

@ -623,13 +623,15 @@ class ChoiceWidget(Widget):
checked_attribute = {"checked": True}
option_inherits_attrs = True
def __init__(self, attrs=None, choices=()):
def __init__(self, attrs=None, choices=(), option_attrs=None):
super().__init__(attrs)
self.choices = choices
self.option_attrs = {} if option_attrs is None else option_attrs.copy()
def __deepcopy__(self, memo):
obj = copy.copy(self)
obj.attrs = self.attrs.copy()
obj.option_attrs = self.option_attrs.copy()
obj.choices = copy.copy(self.choices)
memo[id(self)] = obj
return obj
@ -691,9 +693,14 @@ class ChoiceWidget(Widget):
self, name, value, label, selected, index, subindex=None, attrs=None
):
index = str(index) if subindex is None else "%s_%s" % (index, subindex)
option_attrs = (
self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
)
if self.option_attrs:
option_attrs = self.build_attrs(self.option_attrs)
elif self.option_inherits_attrs:
option_attrs = self.build_attrs(self.attrs, attrs)
else:
option_attrs = {}
if selected:
option_attrs.update(self.checked_attribute)
if "id" in option_attrs:

View File

@ -195,6 +195,18 @@ Django will then include the extra attributes in the rendered output:
You can also set the HTML ``id`` using :attr:`~Widget.attrs`. See
:attr:`BoundField.id_for_label` for an example.
For widgets that have options on their own, like those inheriting from ``ChoiceWidget``,
you can also pass option-specific attributes using ``option_attrs``.
For example, if you wish to set custom CSS classes for a RadioSelect's options, you could
do as follows:
.. code-block:: pycon
>>> from django.forms.widgets import RadioSelect
>>> widget = RadioSelect(choices=(("J", "John"),), option_attrs={"class": "special"})
>>> widget.render(name="beatle", value=["J"])
'<div><div>\n <label><input type="radio" name="beatle" value="J" class="special" checked>\n John</label>\n\n</div>\n</div>'
.. _styling-widget-classes:
Styling widget classes

View File

@ -41,6 +41,8 @@ class ChoiceWidgetTest(WidgetTest):
self.assertIsNot(widget.choices, obj.choices)
self.assertEqual(widget.attrs, obj.attrs)
self.assertIsNot(widget.attrs, obj.attrs)
self.assertEqual(widget.option_attrs, obj.option_attrs)
self.assertIsNot(widget.option_attrs, obj.option_attrs)
def test_options(self):
options = list(

View File

@ -179,6 +179,42 @@ class RadioSelectTest(ChoiceWidgetTest):
"""
self.check_html(widget, "beatle", "J", html=html)
def test_constructor_option_attrs(self):
"""
Attributes provided at instantiation are passed to the constituent
inputs.
"""
widget = self.widget(
attrs={"id": "foo"},
option_attrs={"data-test": "custom", "class": "other"},
choices=self.beatles,
)
html = """
<div id="foo">
<div>
<label>
<input checked class="other" data-test="custom" type="radio"
value="J" name="beatle">John</label>
</div>
<div>
<label>
<input class="other" data-test="custom" type="radio"
value="P" name="beatle">Paul</label>
</div>
<div>
<label>
<input class="other" data-test="custom" type="radio"
value="G" name="beatle">George</label>
</div>
<div>
<label>
<input class="other" data-test="custom" type="radio"
value="R" name="beatle">Ringo</label>
</div>
</div>
"""
self.check_html(widget, "beatle", "J", html=html)
def test_compare_to_str(self):
"""
The value is compared to its str().
@ -491,6 +527,38 @@ class RadioSelectTest(ChoiceWidgetTest):
html=html,
)
def test_render_as_subwidget_with_option_attrs(self):
"""We render option_attrs for the subwidget."""
choices = (("", "------"),) + self.beatles
widget_instance = self.widget(
choices=choices,
option_attrs={"class": "special"},
)
self.check_html(
MultiWidget([widget_instance]),
"beatle",
["J"],
html="""
<div>
<div><label>
<input type="radio" name="beatle_0" value=""
class="special"> ------</label></div>
<div><label>
<input checked type="radio" name="beatle_0" value="J"
class="special"> John</label></div>
<div><label>
<input type="radio" name="beatle_0" value="P"
class="special"> Paul</label></div>
<div><label>
<input type="radio" name="beatle_0" value="G"
class="special"> George</label></div>
<div><label>
<input type="radio" name="beatle_0" value="R"
class="special"> Ringo</label></div>
</div>
""",
)
def test_fieldset(self):
class TestForm(Form):
template_name = "forms_tests/use_fieldset.html"

View File

@ -102,6 +102,28 @@ class SelectTest(ChoiceWidgetTest):
),
)
def test_constructor_option_attrs(self):
"""
Select options shouldn't inherit the parent widget attrs.
"""
widget = Select(
attrs={"class": "super", "id": "super"},
option_attrs={"data-test": "custom", "class": "other"},
choices=[(1, 1), (2, 2), (3, 3)],
)
self.check_html(
widget,
"num",
2,
html=(
"""<select name="num" class="super" id="super">
<option value="1" data-test="custom" class="other">1</option>
<option value="2" data-test="custom" class="other" selected>2</option>
<option value="3" data-test="custom" class="other">3</option>
</select>"""
),
)
def test_compare_to_str(self):
"""
The value is compared to its str().
@ -414,6 +436,23 @@ class SelectTest(ChoiceWidgetTest):
with self.subTest(choices):
self._test_optgroups(choices)
def test_options_with_option_attrs(self):
options = list(
self.widget(choices=self.beatles, option_attrs={"class": "other"}).options(
"name",
["J"],
attrs={"class": "super"},
)
)
self.assertEqual(len(options), 4)
for option, (i, (value, label)) in zip(options, enumerate(self.beatles)):
self.assertEqual(option["name"], "name")
self.assertEqual(option["value"], value)
self.assertEqual(option["label"], label)
self.assertEqual(option["index"], str(i))
self.assertEqual(option["attrs"]["class"], "other")
self.assertIs(option["selected"], value == "J")
def test_doesnt_render_required_when_impossible_to_select_empty_field(self):
widget = self.widget(choices=[("J", "John"), ("P", "Paul")])
self.assertIs(widget.use_required_attribute(initial=None), False)