Skip to content

Commit 78d5f38

Browse files
committed
Fix bug where input elements were dismounted prematurely
1 parent 998041a commit 78d5f38

File tree

3 files changed

+25
-12
lines changed

3 files changed

+25
-12
lines changed

src/reactpy_django/forms/components.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from reactpy_django.forms.transforms import (
1414
convert_html_props_to_reactjs,
1515
convert_textarea_children_to_prop,
16+
infer_key_from_attributes,
1617
intercept_anchor_links,
1718
set_value_prop_on_select_element,
1819
transform_value_prop_on_input_element,
@@ -56,7 +57,7 @@ def _django_form(
5657
rendered_form, set_rendered_form = hooks.use_state(cast(Union[str, None], None))
5758
uuid = uuid_ref.current
5859

59-
# Check the provided arguments
60+
# Validate the provided arguments
6061
if len(top_children) != top_children_count.current or len(bottom_children) != bottom_children_count.current:
6162
msg = "Dynamically changing the number of top or bottom children is not allowed."
6263
raise ValueError(msg)
@@ -67,14 +68,14 @@ def _django_form(
6768
)
6869
raise TypeError(msg)
6970

70-
# Try to initialize the form with the provided data
71+
# Initialize the form with the provided data
7172
initialized_form = form(data=submitted_data)
7273
form_event = FormEventData(
7374
form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data
7475
)
7576

7677
# Validate and render the form
77-
@hooks.use_effect
78+
@hooks.use_effect(dependencies=[str(submitted_data)])
7879
async def render_form():
7980
"""Forms must be rendered in an async loop to allow database fields to execute."""
8081
if submitted_data:
@@ -85,14 +86,12 @@ async def render_form():
8586
if not success and on_error:
8687
await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event)
8788
if success and auto_save and isinstance(initialized_form, ModelForm):
88-
await database_sync_to_async(initialized_form.save)()
89+
await ensure_async(initialized_form.save)()
8990
set_submitted_data(None)
9091

91-
new_form = await database_sync_to_async(initialized_form.render)(
92-
form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE
92+
set_rendered_form(
93+
await ensure_async(initialized_form.render)(form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE)
9394
)
94-
if new_form != rendered_form:
95-
set_rendered_form(new_form)
9695

9796
async def on_submit_callback(new_data: dict[str, Any]):
9897
"""Callback function provided directly to the client side listener. This is responsible for transmitting
@@ -134,6 +133,7 @@ async def _on_change(_event):
134133
set_value_prop_on_select_element,
135134
transform_value_prop_on_input_element,
136135
intercept_anchor_links,
136+
infer_key_from_attributes,
137137
*extra_transforms,
138138
strict=False,
139139
),

src/reactpy_django/forms/transforms.py

+17
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ def intercept_anchor_links(vdom_tree: VdomDict) -> VdomDict:
6868
return vdom_tree
6969

7070

71+
def infer_key_from_attributes(vdom_tree: VdomDict) -> VdomDict:
72+
"""Infer the node's 'key' by looking at any attributes that should be unique."""
73+
attributes = vdom_tree.get("attributes", {})
74+
75+
# Infer 'key' from 'id'
76+
_id = attributes.get("id")
77+
78+
# Fallback: Infer 'key' from 'name'
79+
if not _id and vdom_tree["tagName"] in {"input", "select", "textarea"}:
80+
_id = attributes.get("name")
81+
82+
if _id:
83+
vdom_tree["key"] = _id
84+
85+
return vdom_tree
86+
87+
7188
def _find_selected_options(vdom_node: Any) -> list[str]:
7289
"""Recursively iterate through the tree to find all <option> tags with the 'selected' prop.
7390
Removes the 'selected' prop and returns a list of the 'value' prop of each selected <option>."""

tests/test_app/tests/test_components.py

-4
Original file line numberDiff line numberDiff line change
@@ -805,10 +805,6 @@ def test_model_form(self):
805805
# Make sure no errors remain
806806
assert len(self.page.query_selector_all(".errorlist")) == 0
807807

808-
# Make sure text field is empty
809-
expect(self.page.locator("#id_text")).to_be_empty()
810-
assert self.page.locator("#id_text").get_attribute("value") == ""
811-
812808
# Check if `auto_save` created the TodoItem's database entry
813809
try:
814810
from test_app.models import TodoItem

0 commit comments

Comments
 (0)