Skip to content

Commit f0702d0

Browse files
committed
Squash some bugs with multi choice and boolean fields
1 parent cf08add commit f0702d0

File tree

3 files changed

+86
-84
lines changed

3 files changed

+86
-84
lines changed

src/reactpy_django/forms/components.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
convert_html_props_to_reactjs,
1515
convert_textarea_children_to_prop,
1616
ensure_input_elements_are_controlled,
17+
intercept_anchor_links,
1718
set_value_prop_on_select_element,
1819
)
19-
from reactpy_django.forms.utils import convert_boolean_fields, convert_choice_fields
20+
from reactpy_django.forms.utils import convert_boolean_fields, convert_multiple_choice_fields
2021

2122
if TYPE_CHECKING:
2223
from collections.abc import Sequence
@@ -79,7 +80,7 @@ def on_submit(_event):
7980
last_changed.set_current(timezone.now())
8081

8182
def on_submit_callback(new_data: dict[str, Any]):
82-
convert_choice_fields(new_data, initialized_form)
83+
convert_multiple_choice_fields(new_data, initialized_form)
8384
convert_boolean_fields(new_data, initialized_form)
8485

8586
# TODO: ReactPy's use_state hook really should be de-duplicating this by itself. Needs upstream fix.
@@ -95,6 +96,7 @@ async def on_change(_event):
9596
convert_textarea_children_to_prop,
9697
set_value_prop_on_select_element,
9798
ensure_input_elements_are_controlled(on_change),
99+
intercept_anchor_links,
98100
strict=False,
99101
)
100102

src/reactpy_django/forms/transforms.py

+73-60
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,9 @@
99

1010
# TODO: Move all this logic to `reactpy.utils._mutate_vdom()` and remove this file.
1111

12-
UNSUPPORTED_PROPS = {"children", "ref", "aria-*", "data-*"}
13-
1412

1513
def convert_html_props_to_reactjs(vdom_tree: VdomDict) -> VdomDict:
1614
"""Transformation that standardizes the prop names to be used in the component."""
17-
1815
if not isinstance(vdom_tree, dict):
1916
return vdom_tree
2017

@@ -38,69 +35,113 @@ def convert_textarea_children_to_prop(vdom_tree: VdomDict) -> VdomDict:
3835
text_content = vdom_tree.pop("children")
3936
text_content = "".join([child for child in text_content if isinstance(child, str)])
4037
default_value = vdom_tree["attributes"].pop("defaultValue", "")
41-
vdom_tree["attributes"]["value"] = text_content or default_value
38+
vdom_tree["attributes"]["defaultValue"] = text_content or default_value
4239

4340
for child in vdom_tree.get("children", []):
4441
convert_textarea_children_to_prop(child)
4542

4643
return vdom_tree
4744

4845

49-
def _find_selected_options(vdom_tree: VdomDict, mutation: Callable) -> list[VdomDict]:
50-
"""Recursively iterate through the tree of dictionaries to find an <option> with the 'selected' prop."""
51-
selected_options = []
52-
46+
def set_value_prop_on_select_element(vdom_tree: VdomDict) -> VdomDict:
47+
"""Use the `value` prop on <select> instead of setting `selected` on <option>."""
5348
if not isinstance(vdom_tree, dict):
54-
return selected_options
49+
return vdom_tree
5550

56-
if vdom_tree["tagName"] == "option" and "attributes" in vdom_tree and "selected" in vdom_tree["attributes"]:
57-
mutation(vdom_tree)
58-
selected_options.append(vdom_tree)
51+
# If the current tag is <select>, remove 'selected' prop from any <option> children and
52+
# instead set the 'value' prop on the <select> tag.
53+
if vdom_tree["tagName"] == "select" and "children" in vdom_tree:
54+
selected_options = _find_selected_options(vdom_tree)
55+
multiple_choice = vdom_tree["attributes"]["multiple"] = bool(vdom_tree["attributes"].get("multiple"))
56+
if selected_options and not multiple_choice:
57+
vdom_tree["attributes"]["defaultValue"] = selected_options[0]
58+
if selected_options and multiple_choice:
59+
vdom_tree["attributes"]["defaultValue"] = selected_options
5960

6061
for child in vdom_tree.get("children", []):
61-
selected_options.extend(_find_selected_options(child, mutation))
62+
set_value_prop_on_select_element(child)
6263

63-
return selected_options
64+
return vdom_tree
6465

6566

66-
def set_value_prop_on_select_element(vdom_tree: VdomDict) -> VdomDict:
67-
"""Use the `value` prop on <select> instead of setting `selected` on <option>."""
67+
def ensure_input_elements_are_controlled(event_func: Callable | None = None) -> Callable:
68+
"""Adds an onChange handler on form <input> elements, since ReactJS doesn't like uncontrolled inputs."""
69+
70+
def mutation(vdom_tree: VdomDict) -> VdomDict:
71+
"""Adds an onChange event handler to all input elements."""
72+
if not isinstance(vdom_tree, dict):
73+
return vdom_tree
74+
75+
vdom_tree.setdefault("eventHandlers", {})
76+
if vdom_tree["tagName"] in {"input", "textarea"}:
77+
if "onChange" in vdom_tree["eventHandlers"]:
78+
pass
79+
elif isinstance(event_func, EventHandler):
80+
vdom_tree["eventHandlers"]["onChange"] = event_func
81+
else:
82+
vdom_tree["eventHandlers"]["onChange"] = EventHandler(
83+
to_event_handler_function(event_func or _do_nothing_event)
84+
)
85+
86+
if "children" in vdom_tree:
87+
for child in vdom_tree["children"]:
88+
mutation(child)
89+
90+
return vdom_tree
91+
92+
return mutation
93+
6894

95+
def intercept_anchor_links(vdom_tree: VdomDict) -> VdomDict:
96+
"""Intercepts anchor links and prevents the default behavior.
97+
This allows ReactPy-Router to handle the navigation instead of the browser."""
6998
if not isinstance(vdom_tree, dict):
7099
return vdom_tree
71100

72-
# If the current tag is <select>, remove 'selected' prop from any <option> children and
73-
# instead set the 'value' prop on the <select> tag.
74-
# TODO: Fix this, is broken
75-
if vdom_tree["tagName"] == "select" and "children" in vdom_tree:
101+
if vdom_tree["tagName"] == "a":
76102
vdom_tree.setdefault("eventHandlers", {})
77-
vdom_tree["eventHandlers"]["onChange"] = EventHandler(to_event_handler_function(do_nothing_event))
78-
selected_options = _find_selected_options(vdom_tree, lambda option: option["attributes"].pop("selected"))
79-
multiple_choice = vdom_tree["attributes"].get("multiple")
80-
if selected_options and not multiple_choice:
81-
vdom_tree["attributes"]["value"] = selected_options[0]["children"][0]
82-
if selected_options and multiple_choice:
83-
vdom_tree["attributes"]["value"] = [option["children"][0] for option in selected_options]
103+
vdom_tree["eventHandlers"]["onClick"] = EventHandler(
104+
to_event_handler_function(_do_nothing_event), prevent_default=True
105+
)
84106

85107
for child in vdom_tree.get("children", []):
86-
set_value_prop_on_select_element(child)
108+
intercept_anchor_links(child)
87109

88110
return vdom_tree
89111

90112

113+
def _find_selected_options(vdom_tree: VdomDict) -> list[str]:
114+
"""Recursively iterate through the tree of dictionaries to find an <option> with the 'selected' prop."""
115+
if not isinstance(vdom_tree, dict):
116+
return []
117+
118+
selected_options = []
119+
if vdom_tree["tagName"] == "option" and "attributes" in vdom_tree:
120+
value = vdom_tree["attributes"].setdefault("value", vdom_tree["children"][0])
121+
122+
if "selected" in vdom_tree["attributes"]:
123+
vdom_tree["attributes"].pop("selected")
124+
selected_options.append(value)
125+
126+
for child in vdom_tree.get("children", []):
127+
selected_options.extend(_find_selected_options(child))
128+
129+
return selected_options
130+
131+
91132
def _normalize_prop_name(prop_name: str) -> str:
92133
"""Standardizes the prop name to be used in the component."""
93134
return REACT_PROP_SUBSTITUTIONS.get(prop_name, prop_name)
94135

95136

96-
def react_props_set(string: str) -> set[str]:
137+
def _react_props_set(string: str) -> set[str]:
97138
"""Extracts the props from a string of React props."""
98139
lines = string.strip().split("\n")
99140
props = set()
100141

101142
for line in lines:
102143
parts = line.split(":", maxsplit=1)
103-
if len(parts) == 2 and parts[0] not in UNSUPPORTED_PROPS:
144+
if len(parts) == 2 and parts[0] not in {"children", "ref", "aria-*", "data-*"}:
104145
key, value = parts
105146
key = key.strip()
106147
value = value.strip()
@@ -130,35 +171,7 @@ def _add_on_change_event(event_func, vdom_tree: VdomDict) -> VdomDict:
130171
return vdom_tree
131172

132173

133-
def ensure_input_elements_are_controlled(event_func: Callable | None = None) -> Callable:
134-
"""Adds an onChange handler on form <input> elements, since ReactJS doesn't like uncontrolled inputs."""
135-
136-
def mutation(vdom_tree: VdomDict) -> VdomDict:
137-
"""Adds an onChange event handler to all input elements."""
138-
if not isinstance(vdom_tree, dict):
139-
return vdom_tree
140-
141-
vdom_tree.setdefault("eventHandlers", {})
142-
if vdom_tree["tagName"] in {"input", "textarea"}:
143-
if "onChange" in vdom_tree["eventHandlers"]:
144-
pass
145-
elif isinstance(event_func, EventHandler):
146-
vdom_tree["eventHandlers"]["onChange"] = event_func
147-
else:
148-
vdom_tree["eventHandlers"]["onChange"] = EventHandler(
149-
to_event_handler_function(event_func or do_nothing_event)
150-
)
151-
152-
if "children" in vdom_tree:
153-
for child in vdom_tree["children"]:
154-
mutation(child)
155-
156-
return vdom_tree
157-
158-
return mutation
159-
160-
161-
def do_nothing_event(*args, **kwargs):
174+
def _do_nothing_event(*args, **kwargs):
162175
pass
163176

164177

@@ -495,7 +508,7 @@ def do_nothing_event(*args, **kwargs):
495508
type: a string. Says whether the script is a classic script, ES module, or import map.
496509
"""
497510

498-
KNOWN_REACT_PROPS = react_props_set(
511+
KNOWN_REACT_PROPS = _react_props_set(
499512
SPECIAL_PROPS
500513
+ STANDARD_PROPS
501514
+ FORM_PROPS

src/reactpy_django/forms/utils.py

+9-22
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,24 @@
11
from typing import Any
22

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

55

6-
def convert_choice_fields(data: dict[str, Any], initialized_form: Form) -> None:
7-
choice_field_map = {
8-
field_name: {choice_value: choice_key for choice_key, choice_value in field.choices}
9-
for field_name, field in initialized_form.fields.items()
10-
if isinstance(field, ChoiceField)
11-
}
6+
def convert_multiple_choice_fields(data: dict[str, Any], initialized_form: Form) -> None:
127
multi_choice_fields = {
138
field_name for field_name, field in initialized_form.fields.items() if isinstance(field, MultipleChoiceField)
149
}
1510

16-
# Choice fields submit their values as text, but Django choice keys are not always equal to their values.
17-
# Due to this, we need to convert the text into keys that Django would be happy with
18-
for choice_field_name, choice_map in choice_field_map.items():
19-
if choice_field_name in data:
20-
submitted_value = data[choice_field_name]
21-
if isinstance(submitted_value, list):
22-
data[choice_field_name] = [
23-
choice_map.get(submitted_value_item, submitted_value_item)
24-
for submitted_value_item in submitted_value
25-
]
26-
elif choice_field_name in multi_choice_fields:
27-
data[choice_field_name] = [choice_map.get(submitted_value, submitted_value)]
28-
else:
29-
data[choice_field_name] = choice_map.get(submitted_value, submitted_value)
11+
# Convert multiple choice field text into a list of values
12+
for choice_field_name in multi_choice_fields:
13+
if choice_field_name in data and not isinstance(data[choice_field_name], list):
14+
data[choice_field_name] = [data[choice_field_name]]
3015

3116

3217
def convert_boolean_fields(data: dict[str, Any], initialized_form: Form) -> None:
3318
boolean_fields = {
34-
field_name for field_name, field in initialized_form.fields.items() if isinstance(field, BooleanField)
19+
field_name
20+
for field_name, field in initialized_form.fields.items()
21+
if isinstance(field, BooleanField) and not isinstance(field, NullBooleanField)
3522
}
3623

3724
# Convert boolean field text into actual booleans

0 commit comments

Comments
 (0)