Skip to content

Commit 88ba295

Browse files
committed
Refactoring related to new docs
1 parent 9c28f97 commit 88ba295

File tree

3 files changed

+81
-29
lines changed

3 files changed

+81
-29
lines changed

src/reactpy_django/components.py

+30-8
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from django.forms import Form, ModelForm
3131
from django.views import View
3232

33-
from reactpy_django.types import FormEvent
33+
from reactpy_django.types import AsyncFormEvent, SyncFormEvent
3434

3535

3636
def view_to_component(
@@ -121,23 +121,45 @@ def django_js(static_path: str, key: Key | None = None):
121121
def django_form(
122122
form: type[Form | ModelForm],
123123
*,
124-
on_success: Callable[[FormEvent], None] | None = None,
125-
on_error: Callable[[FormEvent], None] | None = None,
126-
on_submit: Callable[[FormEvent], None] | None = None,
127-
on_change: Callable[[FormEvent], None] | None = None,
124+
on_success: AsyncFormEvent | SyncFormEvent | None = None,
125+
on_error: AsyncFormEvent | SyncFormEvent | None = None,
126+
on_receive_data: AsyncFormEvent | SyncFormEvent | None = None,
127+
on_change: AsyncFormEvent | SyncFormEvent | None = None,
128128
auto_save: bool = True,
129129
extra_props: dict[str, Any] | None = None,
130130
extra_transforms: Sequence[Callable[[VdomDict], Any]] | None = None,
131131
form_template: str | None = None,
132-
top_children: Sequence = (),
133-
bottom_children: Sequence = (),
132+
top_children: Sequence[Any] = (),
133+
bottom_children: Sequence[Any] = (),
134134
key: Key | None = None,
135135
):
136+
"""Converts a Django form to a ReactPy component.
137+
138+
Args:
139+
form: The form instance to convert.
140+
141+
Keyword Args:
142+
on_success: A callback function that is called when the form is successfully submitted.
143+
on_error: A callback function that is called when the form submission fails.
144+
on_receive_data: A callback function that is called before newly submitted form data is rendered.
145+
on_change: A callback function that is called when the form is changed.
146+
auto_save: If `True`, the form will automatically call `save` on successful submission of \
147+
a `ModelForm`. This has no effect on regular `Form` instances.
148+
extra_props: Additional properties to add to the `html.form` element.
149+
extra_transforms: A list of functions that transforms the newly generated VDOM. \
150+
The functions will be repeatedly called on each VDOM node.
151+
form_template: The template to use for the form. If `None`, Django's default template is used.
152+
top_children: Additional elements to add to the top of the form.
153+
bottom_children: Additional elements to add to the bottom of the form.
154+
key: A key to uniquely identify this component which is unique amongst a component's \
155+
immediate siblings.
156+
"""
157+
136158
return _django_form(
137159
form=form,
138160
on_success=on_success,
139161
on_error=on_error,
140-
on_submit=on_submit,
162+
on_receive_data=on_receive_data,
141163
on_change=on_change,
142164
auto_save=auto_save,
143165
extra_props=extra_props or {},

src/reactpy_django/forms/components.py

+40-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
from pathlib import Path
45
from typing import TYPE_CHECKING, Any, Callable, Union, cast
56
from uuid import uuid4
@@ -18,7 +19,7 @@
1819
transform_value_prop_on_input_element,
1920
)
2021
from reactpy_django.forms.utils import convert_boolean_fields, convert_multiple_choice_fields
21-
from reactpy_django.types import FormEvent
22+
from reactpy_django.types import AsyncFormEvent, FormEventData, SyncFormEvent
2223

2324
if TYPE_CHECKING:
2425
from collections.abc import Sequence
@@ -31,13 +32,14 @@
3132
)
3233

3334

35+
# TODO: Create types for AsyncFormEvent and FormEvent
3436
@component
3537
def _django_form(
3638
form: type[Form | ModelForm],
37-
on_success: Callable[[FormEvent], None] | None,
38-
on_error: Callable[[FormEvent], None] | None,
39-
on_submit: Callable[[FormEvent], None] | None,
40-
on_change: Callable[[FormEvent], None] | None,
39+
on_success: AsyncFormEvent | SyncFormEvent | None,
40+
on_error: AsyncFormEvent | SyncFormEvent | None,
41+
on_receive_data: AsyncFormEvent | SyncFormEvent | None,
42+
on_change: AsyncFormEvent | SyncFormEvent | None,
4143
auto_save: bool,
4244
extra_props: dict,
4345
extra_transforms: Sequence[Callable[[VdomDict], Any]],
@@ -64,13 +66,12 @@ def _django_form(
6466
"Do NOT initialize your form by calling it (ex. `MyForm()`)."
6567
)
6668
raise TypeError(msg)
67-
if "id" in extra_props:
68-
msg = "The `extra_props` argument cannot contain an `id` key."
69-
raise ValueError(msg)
7069

7170
# Try to initialize the form with the provided data
7271
initialized_form = form(data=submitted_data)
73-
form_event = FormEvent(form=initialized_form, data=submitted_data or {}, set_data=set_submitted_data)
72+
form_event = FormEventData(
73+
form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data
74+
)
7475

7576
# Validate and render the form
7677
@hooks.use_effect
@@ -80,9 +81,15 @@ async def render_form():
8081
await database_sync_to_async(initialized_form.full_clean)()
8182
success = not initialized_form.errors.as_data()
8283
if success and on_success:
83-
on_success(form_event)
84+
if asyncio.iscoroutinefunction(on_success):
85+
await on_success(form_event)
86+
else:
87+
on_success(form_event)
8488
if not success and on_error:
85-
on_error(form_event)
89+
if asyncio.iscoroutinefunction(on_error):
90+
await on_error(form_event)
91+
else:
92+
on_error(form_event)
8693
if success and auto_save and isinstance(initialized_form, ModelForm):
8794
await database_sync_to_async(initialized_form.save)()
8895
set_submitted_data(None)
@@ -93,28 +100,43 @@ async def render_form():
93100
if new_form != rendered_form:
94101
set_rendered_form(new_form)
95102

96-
def on_submit_callback(new_data: dict[str, Any]):
103+
async def on_submit_callback(new_data: dict[str, Any]):
97104
"""Callback function provided directly to the client side listener. This is responsible for transmitting
98105
the submitted form data to the server for processing."""
99106
convert_multiple_choice_fields(new_data, initialized_form)
100107
convert_boolean_fields(new_data, initialized_form)
101108

102-
if on_submit:
103-
on_submit(FormEvent(form=initialized_form, data=new_data, set_data=set_submitted_data))
109+
if on_receive_data:
110+
new_form_event = FormEventData(
111+
form=initialized_form, submitted_data=new_data, set_submitted_data=set_submitted_data
112+
)
113+
if asyncio.iscoroutinefunction(on_receive_data):
114+
await on_receive_data(new_form_event)
115+
else:
116+
on_receive_data(new_form_event)
104117

105118
if submitted_data != new_data:
106119
set_submitted_data(new_data)
107120

121+
async def _on_change(_event):
122+
"""Event that exist solely to allow the user to detect form changes."""
123+
if on_change:
124+
if asyncio.iscoroutinefunction(on_change):
125+
await on_change(form_event)
126+
else:
127+
on_change(form_event)
128+
108129
if not rendered_form:
109130
return None
110131

111132
return html.form(
112-
{
133+
extra_props
134+
| {
113135
"id": f"reactpy-{uuid}",
136+
# Intercept the form submission to prevent the browser from navigating
114137
"onSubmit": event(lambda _: None, prevent_default=True),
115-
"onChange": on_change(form_event) if on_change else lambda _: None,
116-
}
117-
| extra_props,
138+
"onChange": _on_change,
139+
},
118140
DjangoForm({"onSubmitCallback": on_submit_callback, "formId": f"reactpy-{uuid}"}),
119141
*top_children,
120142
utils.html_to_vdom(

src/reactpy_django/types.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,20 @@ def __call__(self, *args: FuncParams.args, **kwargs: FuncParams.kwargs) -> None:
5454

5555

5656
@dataclass
57-
class FormEvent:
57+
class FormEventData:
5858
"""State of a form provided to Form custom events."""
5959

6060
form: Form | ModelForm
61-
data: dict[str, Any]
62-
set_data: Callable[[dict[str, Any] | None], None]
61+
submitted_data: dict[str, Any]
62+
set_submitted_data: Callable[[dict[str, Any] | None], None]
63+
64+
65+
class AsyncFormEvent(Protocol):
66+
async def __call__(self, event: FormEventData) -> None: ...
67+
68+
69+
class SyncFormEvent(Protocol):
70+
def __call__(self, event: FormEventData) -> None: ...
6371

6472

6573
class AsyncPostprocessor(Protocol):

0 commit comments

Comments
 (0)