diff --git a/fasthtml/components.py b/fasthtml/components.py
index 9296fba3..9aa584cc 100644
--- a/fasthtml/components.py
+++ b/fasthtml/components.py
@@ -171,6 +171,8 @@ def _fill_item(item, obj):
else: attr['value'] = val
if tag=='textarea': cs=(val,)
if tag == 'select':
+ for opt in cs:
+ if opt.tag == 'option': opt.attrs.pop('selected', None)
if isinstance(val, list):
for opt in cs:
if opt.tag == 'option' and opt.get('value') in val:
diff --git a/nbs/api/01_components.ipynb b/nbs/api/01_components.ipynb
index 4e58cb22..2b0a42cc 100644
--- a/nbs/api/01_components.ipynb
+++ b/nbs/api/01_components.ipynb
@@ -1020,6 +1020,8 @@
" else: attr['value'] = val\n",
" if tag=='textarea': cs=(val,)\n",
" if tag == 'select':\n",
+ " for opt in cs:\n",
+ " if opt.tag == 'option': opt.attrs.pop('selected', None)\n",
" if isinstance(val, list):\n",
" for opt in cs:\n",
" if opt.tag == 'option' and opt.get('value') in val:\n",
@@ -1128,6 +1130,73 @@
"multiform"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "e75bc116",
+ "metadata": {},
+ "source": [
+ "Supplied select values replace existing selections, including defaults. An empty list clears them; missing or `None` values and controls with `skip` retain their selections."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2fd0d42a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def selected_values(form): return lx.fromstring(to_xml(form)).xpath('//option[@selected]/@value')\n",
+ "\n",
+ "test_eq(selected_values(multiform), ['a', 'c'])\n",
+ "multiform = fill_form(multiform, {'items': ['b']})\n",
+ "test_eq(selected_values(multiform), ['b'])\n",
+ "multiform = fill_form(multiform, {'items': []})\n",
+ "test_eq(selected_values(multiform), [])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d815c231",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def default_select(**kw):\n",
+ " return Form(Select(Option('a', value='a', selected=True, data_foo='bar'), Option('b', value='b'), name='items', **kw))\n",
+ "\n",
+ "for val,expected in [(['b'], ['b']), ([], []), ('b', ['b']), ('missing', [])]:\n",
+ " filled = fill_form(default_select(multiple=True), {'items': val})\n",
+ " test_eq(selected_values(filled), expected)\n",
+ " test_eq(lx.fromstring(to_xml(filled)).xpath('//option[@value=\"a\"]/@data-foo'), ['bar'])\n",
+ "for obj in ({}, {'items': None}): test_eq(selected_values(fill_form(default_select(), obj)), ['a'])\n",
+ "test_eq(selected_values(fill_form(default_select(skip=True), {'items': ['b']})), ['a'])\n",
+ "\n",
+ "scalarform = Form(Select(Option('a', value='a', selected=True), Option('first b', value='b'),\n",
+ " Option('second b', value='b', selected=True), name='items'))\n",
+ "scalarform = fill_form(scalarform, {'items': 'b'})\n",
+ "test_eq(lx.fromstring(to_xml(scalarform)).xpath('//option[@selected]/text()'), ['first b'])\n",
+ "test_eq(selected_values(fill_form(scalarform, {'items': 'missing'})), [])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3179b5ef",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "controls = Form(Input(type='checkbox', name='checks', value='a', checked=True),\n",
+ " Input(type='checkbox', name='checks', value='b'),\n",
+ " Input(type='radio', name='radio', value='a', checked=True), Input(type='radio', name='radio', value='b'),\n",
+ " Input(name='title', value='old', data_foo='bar'), Textarea('old', name='details'))\n",
+ "controls = fill_form(controls, {'checks': ['b'], 'radio': 'b', 'title': 'new', 'details': 'new details'})\n",
+ "controls = lx.fromstring(to_xml(controls))\n",
+ "test_eq(controls.xpath('//input[@checked]/@value'), ['b', 'b'])\n",
+ "test_eq(controls.xpath('//input[@name=\"title\"]/@value'), ['new'])\n",
+ "test_eq(controls.xpath('//input[@name=\"title\"]/@data-foo'), ['bar'])\n",
+ "test_eq(controls.xpath('//textarea/text()'), ['new details'])"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,