diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca6c6c5..14b67ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Don't forget to remove deprecated code on each major release! - Replaced `web.module_from_file`/`web.export` with `reactjs.component_from_file`. - Replaced `reactpy.backend.types` and `reactpy.core.types` imports with `reactpy.types`. - Renamed `Location.pathname` to `Location.path` and `Location.search` to `Location.query_string`. + - Improved `django_form` submission handling to support multi-value form fields (e.g., `MultipleChoiceField`, `MultiValueField`) by fixing a client-side data serialization issue. ### Removed diff --git a/src/js/src/components.ts b/src/js/src/components.ts index f77e41a5..8518125c 100644 --- a/src/js/src/components.ts +++ b/src/js/src/components.ts @@ -10,15 +10,31 @@ export class DjangoForm extends React.Component { event.preventDefault(); const formData = new FormData(form); - // Convert the FormData object to a plain object - const formObject = Object.fromEntries(formData.entries()); + // Accumulate duplicate keys into arrays to support multi-select fields + // (e.g. MultipleChoiceField). Object.fromEntries would silently drop + // duplicate entries, keeping only the last value per key. + const formObject: Record< + string, + FormDataEntryValue | FormDataEntryValue[] + > = {}; + for (const [key, value] of formData.entries()) { + if (Object.prototype.hasOwnProperty.call(formObject, key)) { + const existing = formObject[key]; + if (Array.isArray(existing)) { + existing.push(value); + } else { + formObject[key] = [existing, value]; + } + } else { + formObject[key] = value; + } + } onSubmitCallback(formObject); }; if (form) { form.addEventListener("submit", onSubmitEvent); - // Store cleanup function in instance (this as any)._cleanup = () => { form.removeEventListener("submit", onSubmitEvent); }; diff --git a/src/reactpy_django/forms/components.py b/src/reactpy_django/forms/components.py index fa3897d5..53d4f02b 100644 --- a/src/reactpy_django/forms/components.py +++ b/src/reactpy_django/forms/components.py @@ -3,12 +3,11 @@ from asyncio import iscoroutinefunction from logging import getLogger from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Union, cast +from typing import TYPE_CHECKING, Any, Callable, cast from uuid import uuid4 from django.forms import Form, ModelForm from reactpy import component, hooks, html, utils -from reactpy.core.events import event from reactpy.reactjs import component_from_file from reactpy_django.forms.transforms import ( @@ -57,7 +56,8 @@ def _django_form( top_children_count = hooks.use_ref(len(top_children)) bottom_children_count = hooks.use_ref(len(bottom_children)) submitted_data, set_submitted_data = hooks.use_state({} or None) - rendered_form, set_rendered_form = hooks.use_state(cast("Union[str, None]", None)) + rendered_form, set_rendered_form = hooks.use_state(cast("str | None", None)) + render_count, set_render_count = hooks.use_state(0) # Initialize the form with the provided data validate_form_args(top_children, top_children_count, bottom_children, bottom_children_count, form) @@ -93,6 +93,7 @@ async def render_form(): await ensure_async(initialized_form.save)() set_submitted_data(None) + set_render_count(render_count + 1) set_rendered_form( await ensure_async(initialized_form.render)(form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE) ) @@ -124,10 +125,15 @@ async def _on_change(_event): if not rendered_form: return None - form_props = { + # Note: `key` is intentionally left stable (does not include `render_count`) so the + # client-side `DjangoForm` component is not torn down and re-mounted on every render. + # The `DjangoForm` registers a native `submit` listener that calls `preventDefault()` + # and forwards the submitted FormData via `onSubmitCallback`; keeping that component + # (and its listener) alive across re-renders guarantees the browser never navigates + # away natively, while still letting each submission reach the server. + form_props: dict[str, Any] = { "id": f"reactpy-{uuid}", - # Intercept the form submission to prevent the browser from navigating - "onSubmit": event(lambda _: None, prevent_default=True), + "key": f"reactpy-{uuid}", } if on_change: form_props["onChange"] = _on_change diff --git a/src/reactpy_django/forms/transforms.py b/src/reactpy_django/forms/transforms.py index 141edc37..07fa7111 100644 --- a/src/reactpy_django/forms/transforms.py +++ b/src/reactpy_django/forms/transforms.py @@ -33,17 +33,32 @@ def convert_textarea_children_to_prop(vdom_tree: VdomDict) -> VdomDict: def set_value_prop_on_select_element(vdom_tree: VdomDict) -> VdomDict: - """Use the `value` prop on , remove 'selected' prop from any