From 33b970787a566af7acba31fe4563e17522211971 Mon Sep 17 00:00:00 2001 From: peterolkhov Date: Wed, 11 Nov 2026 15:30:10 -0500 Subject: [PATCH] Fix stale selections when filling select controls --- fasthtml/components.py | 2 ++ nbs/api/01_components.ipynb | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) 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,