Skip to content

Commit 8e2913f

Browse files
committed
Support model choice fields
1 parent edca217 commit 8e2913f

File tree

3 files changed

+37
-16
lines changed

3 files changed

+37
-16
lines changed

src/reactpy_django/forms/components.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

33
from pathlib import Path
4-
from typing import TYPE_CHECKING, Any, Callable
4+
from typing import TYPE_CHECKING, Any, Callable, Union, cast
55
from uuid import uuid4
66

7+
from channels.db import database_sync_to_async
78
from django.forms import Form
89
from reactpy import component, hooks, html, utils
910
from reactpy.core.events import event
@@ -50,6 +51,7 @@ def _django_form(
5051
top_children_count = hooks.use_ref(len(top_children))
5152
bottom_children_count = hooks.use_ref(len(bottom_children))
5253
submitted_data, set_submitted_data = hooks.use_state({} or None)
54+
rendered_form, set_rendered_form = hooks.use_state(cast(Union[str, None], None))
5355
uuid = uuid_ref.current
5456

5557
# Don't allow the count of top and bottom children to change
@@ -69,39 +71,50 @@ def _django_form(
6971
raise TypeError(msg) from e
7072
raise
7173

72-
# Run the form validation, if data was provided
73-
if submitted_data:
74-
initialized_form.full_clean()
75-
success = not initialized_form.errors.as_data()
76-
form_event = FormEvent(form=initialized_form, data=submitted_data or {})
77-
if success and on_success:
78-
on_success(form_event)
79-
if not success and on_error:
80-
on_error(form_event)
74+
# Set up the form event object
75+
form_event = FormEvent(form=initialized_form, data=submitted_data or {})
76+
77+
# Validate and render the form
78+
@hooks.use_effect
79+
async def render_form():
80+
"""Forms must be rendered in an async loop to allow database fields to execute."""
81+
if submitted_data:
82+
await database_sync_to_async(initialized_form.full_clean)()
83+
success = not initialized_form.errors.as_data()
84+
if success and on_success:
85+
on_success(form_event)
86+
if not success and on_error:
87+
on_error(form_event)
88+
89+
set_rendered_form(await database_sync_to_async(initialized_form.render)(form_template))
8190

8291
def _on_change(_event):
8392
if on_change:
84-
on_change(FormEvent(form=initialized_form, data=submitted_data or {}))
93+
on_change(form_event)
8594

8695
def on_submit_callback(new_data: dict[str, Any]):
8796
"""Callback function provided directly to the client side listener. This is responsible for transmitting
8897
the submitted form data to the server for processing."""
8998
convert_multiple_choice_fields(new_data, initialized_form)
9099
convert_boolean_fields(new_data, initialized_form)
91100

101+
if on_submit:
102+
on_submit(FormEvent(form=initialized_form, data=new_data))
103+
92104
# TODO: The `use_state`` hook really should be de-duplicating this by itself. Needs upstream fix.
93105
if submitted_data != new_data:
94-
if on_submit:
95-
on_submit(FormEvent(form=initialized_form, data=new_data))
96106
set_submitted_data(new_data)
97107

108+
if not rendered_form:
109+
return None
110+
98111
return html.form(
99112
{"id": f"reactpy-{uuid}", "onSubmit": event(lambda _: None, prevent_default=True), "onChange": _on_change}
100113
| extra_props,
101114
DjangoForm({"onSubmitCallback": on_submit_callback, "formId": f"reactpy-{uuid}"}),
102115
*top_children,
103116
utils.html_to_vdom(
104-
initialized_form.render(form_template),
117+
rendered_form,
105118
convert_html_props_to_reactjs,
106119
convert_textarea_children_to_prop,
107120
set_value_prop_on_select_element,

src/reactpy_django/forms/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from typing import Any
22

3-
from django.forms import BooleanField, Form, MultipleChoiceField, NullBooleanField
3+
from django.forms import BooleanField, Form, ModelMultipleChoiceField, MultipleChoiceField, NullBooleanField
44

55

66
def convert_multiple_choice_fields(data: dict[str, Any], initialized_form: Form) -> None:
77
multi_choice_fields = {
8-
field_name for field_name, field in initialized_form.fields.items() if isinstance(field, MultipleChoiceField)
8+
field_name
9+
for field_name, field in initialized_form.fields.items()
10+
if isinstance(field, (MultipleChoiceField, ModelMultipleChoiceField))
911
}
1012

1113
# Convert multiple choice field text into a list of values

tests/test_app/forms/forms.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,10 @@ class BasicForm(forms.Form):
3636
label="combo", fields=[forms.CharField(), forms.EmailField()], initial="example@gmail.com"
3737
)
3838
password_field = forms.CharField(label="password", widget=forms.PasswordInput)
39+
model_choice_field = forms.ModelChoiceField(
40+
label="model choice field", initial="1", queryset=models.TodoItem.objects.all()
41+
)
42+
model_multiple_choice_field = forms.ModelMultipleChoiceField(
43+
label="model multiple choice field", initial="1", queryset=models.TodoItem.objects.all()
44+
)
3945

0 commit comments

Comments
 (0)