Skip to content

Commit 3863eb2

Browse files
committed
Add bootstrap form
1 parent 66fa334 commit 3863eb2

File tree

10 files changed

+103
-40
lines changed

10 files changed

+103
-40
lines changed

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ extra-dependencies = [
9494
"twisted",
9595
"tblib",
9696
"servestatic",
97+
"django-bootstrap5",
9798
]
9899
matrix-name-format = "{variable}-{value}"
99100

@@ -140,7 +141,12 @@ pythonpath = [".", "tests/"]
140141
################################
141142

142143
[tool.hatch.envs.django]
143-
extra-dependencies = ["channels[daphne]>=4.0.0", "twisted", "servestatic"]
144+
extra-dependencies = [
145+
"channels[daphne]>=4.0.0",
146+
"twisted",
147+
"servestatic",
148+
"django-bootstrap5",
149+
]
144150

145151
[tool.hatch.envs.django.scripts]
146152
runserver = [

src/reactpy_django/components.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,22 @@ def django_js(static_path: str, key: Key | None = None):
117117

118118

119119
def django_form(
120-
form: type[Form], *, top_children: Sequence = (), bottom_children: Sequence = (), key: Key | None = None
120+
form: type[Form],
121+
*,
122+
extra_props: dict[str, Any] | None = None,
123+
form_template: str | None = None,
124+
top_children: Sequence = (),
125+
bottom_children: Sequence = (),
126+
key: Key | None = None,
121127
):
122-
return _django_form(form=form, top_children=top_children, bottom_children=bottom_children, key=key)
128+
return _django_form(
129+
form=form,
130+
extra_props=extra_props or {},
131+
form_template=form_template,
132+
top_children=top_children,
133+
bottom_children=bottom_children,
134+
key=key,
135+
)
123136

124137

125138
def pyscript_component(
Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from __future__ import annotations
22

33
from pathlib import Path
4-
from pprint import pprint
54
from typing import TYPE_CHECKING, Any
65
from uuid import uuid4
76

87
from django.forms import Form
9-
from django.utils import timezone
108
from reactpy import component, hooks, html, utils
119
from reactpy.core.events import event
1210
from reactpy.web import export, module_from_file
@@ -30,20 +28,20 @@
3028

3129

3230
@component
33-
def _django_form(form: type[Form], top_children: Sequence, bottom_children: Sequence):
34-
# TODO: Implement form restoration on page reload. Probably want to create a new setting called
35-
# form_restoration_method that can be set to "URL", "CLIENT_STORAGE", "SERVER_SESSION", or None.
36-
# Or maybe just recommend pre-rendering to have the browser handle it.
37-
# Be clear that URL mode will limit you to one form per page.
38-
# TODO: Test this with django-bootstrap, django-colorfield, django-ace, django-crispy-forms
39-
# TODO: Add pre-submit and post-submit hooks
31+
def _django_form(
32+
form: type[Form], extra_props: dict, form_template: str | None, top_children: Sequence, bottom_children: Sequence
33+
):
34+
# TODO: Implement form restoration on page reload. Maybe this involves creating a new setting called
35+
# `form_restoration_method` that can be set to "URL", "CLIENT_STORAGE", "SERVER_SESSION", or None.
36+
# Perhaps pre-rendering is robust enough already handle this scenario?
37+
# Additionaly, "URL" mode would limit the user to one form per page.
38+
# TODO: Test this with django-colorfield, django-ace, django-crispy-forms
39+
# TODO: Add pre-submit, post-submit, error, and success hooks
4040
# TODO: Add auto-save option for database-backed forms
4141
uuid_ref = hooks.use_ref(uuid4().hex.replace("-", ""))
4242
top_children_count = hooks.use_ref(len(top_children))
4343
bottom_children_count = hooks.use_ref(len(bottom_children))
4444
submitted_data, set_submitted_data = hooks.use_state({} or None)
45-
last_changed = hooks.use_ref(timezone.now())
46-
4745
uuid = uuid_ref.current
4846

4947
# Don't allow the count of top and bottom children to change
@@ -61,42 +59,35 @@ def _django_form(form: type[Form], top_children: Sequence, bottom_children: Sequ
6159
"Do NOT initialize your form by calling it (ex. `MyForm()`)."
6260
)
6361
raise TypeError(msg) from e
64-
raise
62+
raise e
6563

6664
# Run the form validation, if data was provided
6765
if submitted_data:
6866
initialized_form.full_clean()
69-
70-
@event(prevent_default=True)
71-
def on_submit(_event):
72-
"""The server was notified that a form was submitted. Note that actual submission behavior is handled by `on_submit_callback`."""
73-
last_changed.set_current(timezone.now())
67+
print("Form errors:", initialized_form.errors.as_data())
7468

7569
def on_submit_callback(new_data: dict[str, Any]):
70+
"""Callback function provided directly to the client side listener. This is responsible for transmitting
71+
the submitted form data to the server for processing."""
7672
convert_multiple_choice_fields(new_data, initialized_form)
7773
convert_boolean_fields(new_data, initialized_form)
7874

79-
# TODO: ReactPy's use_state hook really should be de-duplicating this by itself. Needs upstream fix.
75+
# TODO: The `use_state`` hook really should be de-duplicating this by itself. Needs upstream fix.
8076
if submitted_data != new_data:
8177
set_submitted_data(new_data)
8278

83-
def on_change(_event):
84-
last_changed.set_current(timezone.now())
85-
86-
rendered_form = utils.html_to_vdom(
87-
initialized_form.render(),
88-
convert_html_props_to_reactjs,
89-
convert_textarea_children_to_prop,
90-
set_value_prop_on_select_element,
91-
ensure_input_elements_are_controlled(on_change),
92-
intercept_anchor_links,
93-
strict=False,
94-
)
95-
9679
return html.form(
97-
{"id": f"reactpy-{uuid}", "onSubmit": on_submit},
80+
{"id": f"reactpy-{uuid}", "onSubmit": event(lambda _: None, prevent_default=True)} | extra_props,
9881
DjangoForm({"onSubmitCallback": on_submit_callback, "formId": f"reactpy-{uuid}"}),
9982
*top_children,
100-
rendered_form,
83+
utils.html_to_vdom(
84+
initialized_form.render(form_template),
85+
convert_html_props_to_reactjs,
86+
convert_textarea_children_to_prop,
87+
set_value_prop_on_select_element,
88+
ensure_input_elements_are_controlled(),
89+
intercept_anchor_links,
90+
strict=False,
91+
),
10192
*bottom_children,
10293
)

tests/test_app/forms/components.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,14 @@
66

77

88
@component
9-
def basic_form(form_template=None):
10-
return django_form(BasicForm, form_template=form_template, bottom_children=(html.input({"type": "submit"}),))
9+
def basic_form():
10+
return django_form(BasicForm, bottom_children=(html.input({"type": "submit"}),))
11+
12+
13+
@component
14+
def bootstrap_form():
15+
return django_form(
16+
BasicForm,
17+
extra_props={"style": {"maxWidth": "600px", "margin": "auto"}},
18+
form_template="bootstrap_form_template.html",
19+
)

tests/test_app/forms/urls.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from django.urls import path
22

3-
from .views import form
3+
from . import views
44

55
urlpatterns = [
6-
path("form/", form),
6+
path("form/", views.form),
7+
path("form/bootstrap/", views.bootstrap_form),
78
]

tests/test_app/forms/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
def form(request):
55
return render(request, "form.html", {})
6+
7+
8+
def bootstrap_form(request):
9+
return render(request, "bootstrap_form.html", {})

tests/test_app/settings_multi_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"django.contrib.staticfiles",
2828
"reactpy_django", # Django compatiblity layer for ReactPy
2929
"test_app", # This test application
30+
"django_bootstrap5",
3031
]
3132
MIDDLEWARE = [
3233
"django.middleware.security.SecurityMiddleware",

tests/test_app/settings_single_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"django.contrib.staticfiles",
2828
"reactpy_django", # Django compatiblity layer for ReactPy
2929
"test_app", # This test application
30+
"django_bootstrap5",
3031
]
3132
MIDDLEWARE = [
3233
"django.middleware.security.SecurityMiddleware",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% load static %} {% load reactpy %}
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
5+
<head>
6+
<meta charset="UTF-8" />
7+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
8+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
9+
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
10+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
11+
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
12+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
13+
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
14+
crossorigin="anonymous"></script>
15+
<title>ReactPy</title>
16+
<style>
17+
iframe {
18+
width: 100%;
19+
height: 45px;
20+
}
21+
</style>
22+
</head>
23+
24+
<body>
25+
<h1>ReactPy Bootstrap Form Test Page</h1>
26+
<hr>
27+
{% component "test_app.forms.components.bootstrap_form" %}
28+
<hr>
29+
</body>
30+
31+
</html>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{% load django_bootstrap5 %}
2+
3+
{% csrf_token %}
4+
{% bootstrap_form form %}
5+
{% bootstrap_button button_type="submit" content="OK" %}
6+
{% bootstrap_button button_type="reset" content="Cancel" %}

0 commit comments

Comments
 (0)