Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ Don't forget to remove deprecated code on each major release!
- Removed `reactpy_django/html.py` (`pyscript` VDOM constructor). Use `html.py_script` instead.
- Removed `nest_asyncio` dependency and its initialization on startup.

### Fixed

- Resolved bug where `django_form` events would sometimes not occur.

### [5.2.1] - 2025-01-10

### Changed
Expand Down
18 changes: 14 additions & 4 deletions src/reactpy_django/forms/components.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from asyncio import iscoroutinefunction
from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Union, cast
Expand Down Expand Up @@ -65,6 +66,15 @@ def _django_form(
form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data
)

async def _call_event_callback(callback, event_data):
"""Call a form event callback, running sync callbacks directly
(not through database_sync_to_async) to avoid issues with ReactPy state
setters that require an active event loop."""
if iscoroutinefunction(callback):
await callback(event_data)
else:
callback(event_data)

# Validate and render the form
@hooks.use_async_effect(dependencies=[str(submitted_data)])
async def render_form():
Expand All @@ -76,9 +86,9 @@ async def render_form():
await ensure_async(initialized_form.full_clean, thread_sensitive=thread_sensitive)()
success = not initialized_form.errors.as_data()
if success and on_success:
await ensure_async(on_success, thread_sensitive=thread_sensitive)(form_event)
await _call_event_callback(on_success, form_event)
if not success and on_error:
await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event)
await _call_event_callback(on_error, form_event)
if success and auto_save and isinstance(initialized_form, ModelForm):
await ensure_async(initialized_form.save)()
set_submitted_data(None)
Expand All @@ -101,15 +111,15 @@ async def on_submit_callback(new_data: dict[str, Any]):
new_form_event = FormEventData(
form=initialized_form, submitted_data=new_data, set_submitted_data=set_submitted_data
)
await ensure_async(on_receive_data, thread_sensitive=thread_sensitive)(new_form_event)
await _call_event_callback(on_receive_data, new_form_event)

if submitted_data != new_data:
set_submitted_data(new_data)

async def _on_change(_event):
"""Event that exist solely to allow the user to detect form changes."""
if on_change:
await ensure_async(on_change, thread_sensitive=thread_sensitive)(form_event)
await _call_event_callback(on_change, form_event)

if not rendered_form:
return None
Expand Down
117 changes: 57 additions & 60 deletions tests/test_app/tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -859,63 +859,60 @@ def test_form_orm_model(self):
finally:
os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE")

# FIXME: Re-enable once we fix the following:
# https://github.com/reactive-python/reactpy-django/issues/272

# @navigate_to_page("/form/sync_event/")
# def test_form_sync_events(self):
# self.page.wait_for_selector("form")

# # Check initial state
# self.page.wait_for_selector("#success[data-value='false']")
# self.page.wait_for_selector("#error[data-value='false']")
# self.page.wait_for_selector("#receive_data[data-value='false']")
# self.page.wait_for_selector("#change[data-value='false']")

# # Submit empty the form
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# # The empty form was submitted, should result in an error
# self.page.wait_for_selector("#success[data-value='false']")
# self.page.wait_for_selector("#error[data-value='true']")
# self.page.wait_for_selector("#receive_data[data-value='true']")
# self.page.wait_for_selector("#change[data-value='false']")

# # Fill out the form and re-submit
# self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# # Form should have been successfully submitted
# self.page.wait_for_selector("#success[data-value='true']")
# self.page.wait_for_selector("#error[data-value='true']")
# self.page.wait_for_selector("#receive_data[data-value='true']")
# self.page.wait_for_selector("#change[data-value='true']")

# @navigate_to_page("/form/async_event/")
# def test_form_async_events(self):
# self.page.wait_for_selector("form")

# # Check initial state
# self.page.wait_for_selector("#success[data-value='false']")
# self.page.wait_for_selector("#error[data-value='false']")
# self.page.wait_for_selector("#receive_data[data-value='false']")
# self.page.wait_for_selector("#change[data-value='false']")

# # Submit empty the form
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# # The empty form was submitted, should result in an error
# self.page.wait_for_selector("#success[data-value='false']")
# self.page.wait_for_selector("#error[data-value='true']")
# self.page.wait_for_selector("#receive_data[data-value='true']")
# self.page.wait_for_selector("#change[data-value='false']")

# # Fill out the form and re-submit
# self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# # Form should have been successfully submitted
# self.page.wait_for_selector("#success[data-value='true']")
# self.page.wait_for_selector("#error[data-value='true']")
# self.page.wait_for_selector("#receive_data[data-value='true']")
# self.page.wait_for_selector("#change[data-value='true']")
@navigate_to_page("/form/sync_event/")
def test_form_sync_events(self):
self.page.wait_for_selector("form")

# Check initial state
self.page.wait_for_selector("#success[data-value='false']")
self.page.wait_for_selector("#error[data-value='false']")
self.page.wait_for_selector("#receive_data[data-value='false']")
self.page.wait_for_selector("#change[data-value='false']")

# Submit empty the form
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# The empty form was submitted, should result in an error
self.page.wait_for_selector("#success[data-value='false']")
self.page.wait_for_selector("#error[data-value='true']")
self.page.wait_for_selector("#receive_data[data-value='true']")
self.page.wait_for_selector("#change[data-value='false']")

# Fill out the form and re-submit
self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# Form should have been successfully submitted
self.page.wait_for_selector("#success[data-value='true']")
self.page.wait_for_selector("#error[data-value='true']")
self.page.wait_for_selector("#receive_data[data-value='true']")
self.page.wait_for_selector("#change[data-value='true']")

@navigate_to_page("/form/async_event/")
def test_form_async_events(self):
self.page.wait_for_selector("form")

# Check initial state
self.page.wait_for_selector("#success[data-value='false']")
self.page.wait_for_selector("#error[data-value='false']")
self.page.wait_for_selector("#receive_data[data-value='false']")
self.page.wait_for_selector("#change[data-value='false']")

# Submit empty the form
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# The empty form was submitted, should result in an error
self.page.wait_for_selector("#success[data-value='false']")
self.page.wait_for_selector("#error[data-value='true']")
self.page.wait_for_selector("#receive_data[data-value='true']")
self.page.wait_for_selector("#change[data-value='false']")

# Fill out the form and re-submit
self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)

# Form should have been successfully submitted
self.page.wait_for_selector("#success[data-value='true']")
self.page.wait_for_selector("#error[data-value='true']")
self.page.wait_for_selector("#receive_data[data-value='true']")
self.page.wait_for_selector("#change[data-value='true']")
Loading