From 78d5f389868bd75676072338909a2fb9dced6b0e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:04:20 -0800 Subject: [PATCH] Fix bug where input elements were dismounted prematurely --- src/reactpy_django/forms/components.py | 16 ++++++++-------- src/reactpy_django/forms/transforms.py | 17 +++++++++++++++++ tests/test_app/tests/test_components.py | 4 ---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/reactpy_django/forms/components.py b/src/reactpy_django/forms/components.py index 157bed23..d19c0bbb 100644 --- a/src/reactpy_django/forms/components.py +++ b/src/reactpy_django/forms/components.py @@ -13,6 +13,7 @@ from reactpy_django.forms.transforms import ( convert_html_props_to_reactjs, convert_textarea_children_to_prop, + infer_key_from_attributes, intercept_anchor_links, set_value_prop_on_select_element, transform_value_prop_on_input_element, @@ -56,7 +57,7 @@ def _django_form( rendered_form, set_rendered_form = hooks.use_state(cast(Union[str, None], None)) uuid = uuid_ref.current - # Check the provided arguments + # Validate the provided arguments if len(top_children) != top_children_count.current or len(bottom_children) != bottom_children_count.current: msg = "Dynamically changing the number of top or bottom children is not allowed." raise ValueError(msg) @@ -67,14 +68,14 @@ def _django_form( ) raise TypeError(msg) - # Try to initialize the form with the provided data + # Initialize the form with the provided data initialized_form = form(data=submitted_data) form_event = FormEventData( form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data ) # Validate and render the form - @hooks.use_effect + @hooks.use_effect(dependencies=[str(submitted_data)]) async def render_form(): """Forms must be rendered in an async loop to allow database fields to execute.""" if submitted_data: @@ -85,14 +86,12 @@ async def render_form(): if not success and on_error: await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event) if success and auto_save and isinstance(initialized_form, ModelForm): - await database_sync_to_async(initialized_form.save)() + await ensure_async(initialized_form.save)() set_submitted_data(None) - new_form = await database_sync_to_async(initialized_form.render)( - form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE + set_rendered_form( + await ensure_async(initialized_form.render)(form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE) ) - if new_form != rendered_form: - set_rendered_form(new_form) async def on_submit_callback(new_data: dict[str, Any]): """Callback function provided directly to the client side listener. This is responsible for transmitting @@ -134,6 +133,7 @@ async def _on_change(_event): set_value_prop_on_select_element, transform_value_prop_on_input_element, intercept_anchor_links, + infer_key_from_attributes, *extra_transforms, strict=False, ), diff --git a/src/reactpy_django/forms/transforms.py b/src/reactpy_django/forms/transforms.py index 7a9e84f4..2d527209 100644 --- a/src/reactpy_django/forms/transforms.py +++ b/src/reactpy_django/forms/transforms.py @@ -68,6 +68,23 @@ def intercept_anchor_links(vdom_tree: VdomDict) -> VdomDict: return vdom_tree +def infer_key_from_attributes(vdom_tree: VdomDict) -> VdomDict: + """Infer the node's 'key' by looking at any attributes that should be unique.""" + attributes = vdom_tree.get("attributes", {}) + + # Infer 'key' from 'id' + _id = attributes.get("id") + + # Fallback: Infer 'key' from 'name' + if not _id and vdom_tree["tagName"] in {"input", "select", "textarea"}: + _id = attributes.get("name") + + if _id: + vdom_tree["key"] = _id + + return vdom_tree + + def _find_selected_options(vdom_node: Any) -> list[str]: """Recursively iterate through the tree to find all