Skip to content

Fix #270: Resolve django_form limitations for multi-value and multi-select fields - #298

Merged
Archmonger merged 11 commits into
mainfrom
fix-270
Sep 17, 2026
Merged

Archmonger merged 11 commits into
mainfrom
fix-270

Conversation

@Archmonger

@Archmonger Archmonger commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Description

Fix (partial) #270 by resolving the following categories of django_form limitations:

1. FormData serialization bug (MultipleChoiceField data loss)

Object.fromEntries(formData.entries()) silently drops duplicate keys, causing MultipleChoiceField and ModelMultipleChoiceField to lose all values except the last selected option. Replaced with a manual loop that accumulates duplicate keys into FormDataEntryValue[] arrays.

2. Robust convert_form_fields normalization

Improved convert_form_fields() to properly handle edge cases:

  • None values from unselected multi-select fields → []
  • Single values → [value]
  • Already-list values → unchanged

3. Test coverage for multi-value fields

Added SplitDateTimeField and MultiValueField to BasicForm in the test app for regression coverage.

Checklist

  • Tests have been developed for bug fixes or new functionality.
  • The changelog has been updated, if necessary.
  • Documentation has been updated, if necessary.
  • GitHub Issues closed by this PR have been linked.

By submitting this pull request I agree that all contributions comply with this project's open source license(s).

…delMultipleChoiceField

- Updated `components.ts` to accumulate duplicate FormData entries into arrays
  instead of silently dropping duplicates (as Object.fromEntries would do).
- Updated `utils.py` convert_form_fields() to normalize MultipleChoiceField
  and ModelMultipleChoiceField values (absent/None -> empty list, single -> array).
- Updated test forms to include MultipleChoiceField and
  ModelMultipleChoiceField in BasicForm and BootstrapForm for regression coverage.
@Archmonger
Archmonger requested a review from a team as a code owner July 19, 2026 21:56
Archmonger pushed a commit that referenced this pull request Jul 20, 2026
…d-trip assertions

Following code review suggestions for PR #298:
- New test_forms_utils.py: 10 unit tests covering None->[], single->[value],
  list->unchanged for MultipleChoiceField, ModelMultipleChoiceField,
  BooleanField/NullBooleanField, and CharField pass-through.
- test_form_basic: explicit input_value assertions on multi-select and
  model multi-select fields after successful form submission, proving
  the FormData duplicate-key fix works end-to-end.
- Fix pre-existing ruff import ordering issue in utils.py
…d-trip assertions

Following code review suggestions for PR #298:
- New test_forms_utils.py: 10 unit tests covering None->[], single->[value],
  list->unchanged for MultipleChoiceField, ModelMultipleChoiceField,
  BooleanField/NullBooleanField, and CharField pass-through.
- test_form_basic: explicit input_value assertions on multi-select and
  model multi-select fields after successful form submission, proving
  the FormData duplicate-key fix works end-to-end.
- Fix pre-existing ruff import ordering issue in utils.py
@Archmonger
Archmonger force-pushed the fix-270 branch 3 times, most recently from 11be5bf to de12ba0 Compare July 21, 2026 16:04
The set_value_prop_on_select_element transform previously stripped
the selected attribute from option elements, relying solely on
defaultValue for restoring selection. Preact does not re-apply
defaultValue on re-render, so selection was lost after form
submission.

Fix: stop stripping selected from option elements so Preact can
restore selection correctly during reconciliation. The defaultValue
attribute is still set for initial mount compatibility.

Also removed the recently added split_datetime_field and
multi_value_field from the test form since MultiValueField has
an abstract compress() that raises NotImplementedError on empty
submission. Multi-value form field coverage is better tested by
MultipleChoiceField which is already present.
Preact/React does not reliably restore <select multiple> selection
state during in-place reconciliation of option children. Force a
complete unmount/remount of the <form> element by using a volatile
React key that increments each time submitted_data triggers a
re-render.

This ensures defaultValue is applied on mount and that the DOM
select element starts fresh with the correct selection.
@Archmonger Archmonger linked an issue Jul 22, 2026 that may be closed by this pull request
5 tasks
@Archmonger Archmonger removed a link to an issue Sep 15, 2026
5 tasks
User added 2 commits September 15, 2026 04:45
The `django_form` component lost multi-select (MultipleChoiceField) values
after a successful submission because two separate issues prevented the
re-rendered form from restoring the selection on the client:

1. The `onSubmit` ReactPy event handler on the `<form>` element was
   redundant with the client-side `DjangoForm` submit listener (which already
   calls `preventDefault()` and forwards the submitted data). When a form was
   submitted a second time (after re-rendering validation errors), this extra
   handler caused the WebSocket to close and remount the component, dropping
   the submission before it reached the server. Removing the redundant
   `onSubmit` prop lets the client-side listener reliably deliver the data.

2. ReactPy's built-in `select_element_to_reactjs` transform strips the
   `selected` attribute from `<option>` elements and stores it as
   `defaultValue` on the parent `<select>`. Preact does not apply a `<select>`'s
   `defaultValue` prop to its `<option>` children, so the selection was lost on
   re-render. `set_value_prop_on_select_element` now also re-applies `selected`
   on the matching `<option>` elements, which Preact applies as the option's DOM
   `selected` property.

Also fixes the multi-select assertions in `test_form_basic` to read the
selected `<option>` values, since `input_value()` returns only the first
selection on a multi-select.
User added 3 commits September 17, 2026 00:09
Addresses feedback from a code review of the #270 django_form fix:

- Add direct unit tests (tests/test_app/tests/test_transforms.py) for
  `set_value_prop_on_select_element`, covering single-select, multi-select,
  empty selection, a string defaultValue on a multi-select, the `multiple`
  attribute coercion, and the non-select no-op case. This transform is coupled
  to ReactPy's built-in `select_element_to_reactjs` (which runs first and
  records `defaultValue` on the `<select>`), so explicit regression coverage is
  important.
- Document why `onSubmit` is intentionally not attached to the `<form>`: the
  client-side `DjangoForm` already registers a native submit listener that calls
  `preventDefault()` and forwards the data; a redundant ReactPy `onSubmit`
  handler used to close the WebSocket on a second submission and drop the data.
- Clean up a pre-existing `Union[str, None]` cast to use `str | None` and drop
  the now-unused `Union` import in components.py.
Previously the <form>'s key was derived partly from render_count, so the
client-side DjangoForm component (which registers the native submit listener)
was torn down and re-mounted on every render. During that remount window a fast
second submission would not be intercepted, causing the browser to navigate
natively and the form event callbacks (particularly on_receive_data / on_change
in test_form_sync_events and test_form_async_events) to fire unreliably.

Make the form key stable (uuid only) so the DjangoForm and its native submit
listener persist across re-renders. Combined with the multi-select transform
fix, this keeps preventDefault reliable (no native navigation) while still
letting every submission reach the server, fixing both flaky event tests and the
multi-select round-trip in test_form_basic.
Filling char_field triggers the form's onChange handler, which causes an
async server re-render that reconciles (and can transiently remount) the
submit button. A single Playwright click on input[type=submit] can then
land on the form element instead of the button, swallowing the submit
event and losing the valid submission -- so test_form_sync_events and
test_form_async_events intermittently timed out waiting for
#success[data-value='true'].

Make the interaction deterministic by retrying (re-filling + re-clicking)
until the on_success callback visibly takes effect.
@Archmonger
Archmonger merged commit 43527ef into main Sep 17, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant