Skip to content

Django form component #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 50 commits into from
Dec 11, 2024
Merged
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
168a727
Client side form handler
Archmonger Dec 3, 2024
98ba450
misc changelog bump
Archmonger Dec 3, 2024
14c9dde
Functional client code
Archmonger Dec 5, 2024
ebd87bf
First draft of form conversion
Archmonger Dec 5, 2024
164e3a3
format
Archmonger Dec 5, 2024
cf08add
Move code to forms module
Archmonger Dec 5, 2024
f0702d0
Squash some bugs with multi choice and boolean fields
Archmonger Dec 6, 2024
63e23d5
Remove auto submit from the base form
Archmonger Dec 6, 2024
66fa334
Create form test
Archmonger Dec 6, 2024
3863eb2
Add bootstrap form
Archmonger Dec 6, 2024
4565df0
add events for form component
Archmonger Dec 6, 2024
24cc64b
Add on_change event
Archmonger Dec 6, 2024
edca217
simplify ensure_input_elements_are_controlled
Archmonger Dec 6, 2024
8e2913f
Support model choice fields
Archmonger Dec 6, 2024
cc4fd22
Prep work for DB backed form
Archmonger Dec 6, 2024
f0b21c7
Full support for database backed forms
Archmonger Dec 6, 2024
851b113
Fix render loop bug
Archmonger Dec 6, 2024
6bf8c24
quick self review
Archmonger Dec 6, 2024
4077a06
Add changelog
Archmonger Dec 6, 2024
08034fa
Simplify transforms
Archmonger Dec 7, 2024
24ad84c
Add extra transforms arg
Archmonger Dec 7, 2024
9360e51
REACTPY_DEFAULT_FORM_TEMPLATE
Archmonger Dec 7, 2024
7371a45
better input transform
Archmonger Dec 7, 2024
8c9990f
var name cleanup for _find_selected_options
Archmonger Dec 7, 2024
a54f035
cleanup in transform_value_prop_on_input_element
Archmonger Dec 7, 2024
78fdbed
simplify convert_multiple_choice_fields
Archmonger Dec 7, 2024
c8c71c4
Remove unsupported fields comment
Archmonger Dec 7, 2024
282e542
Rename cancel btn to reset
Archmonger Dec 7, 2024
bd58a1b
Move extra props arg
Archmonger Dec 7, 2024
d446161
Set default attributes in transforms
Archmonger Dec 7, 2024
aee08da
Fix edge case where error is thrown on empty choice field
Archmonger Dec 7, 2024
9c28f97
First cut at docs
Archmonger Dec 8, 2024
88ba295
Refactoring related to new docs
Archmonger Dec 8, 2024
a44591f
use local bootstrap for tests
Archmonger Dec 8, 2024
16c04bc
Remove default values from test form
Archmonger Dec 8, 2024
d561c88
self review
Archmonger Dec 8, 2024
d9416d9
Add tests
Archmonger Dec 9, 2024
208ec17
Add readme
Archmonger Dec 9, 2024
602be6b
Try dynamically selecting options for file_path_field
Archmonger Dec 9, 2024
1d3d095
Update todo comments
Archmonger Dec 9, 2024
b6fb5c4
Increase sleep on async relational query test
Archmonger Dec 9, 2024
608acee
Add another check in query tests
Archmonger Dec 9, 2024
3f834ca
Fix default on admin middleware
Archmonger Dec 9, 2024
73967b4
New ensure_async util function
Archmonger Dec 9, 2024
e32fc67
fix bad import
Archmonger Dec 9, 2024
3ed84db
Add thread_sensitive arg to ensure_async func
Archmonger Dec 10, 2024
55daaa2
Add new tests for form events
Archmonger Dec 10, 2024
5eb5818
run formatter
Archmonger Dec 10, 2024
998041a
another simplification of middleware
Archmonger Dec 10, 2024
78d5f38
Fix bug where input elements were dismounted prematurely
Archmonger Dec 11, 2024
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
Prev Previous commit
Next Next commit
Simplify transforms
  • Loading branch information
Archmonger committed Dec 7, 2024
commit 08034fadaae9c069d56cda002d31e4fe01d0bf5c
72 changes: 10 additions & 62 deletions src/reactpy_django/forms/transforms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from reactpy.core.events import EventHandler, to_event_handler_function

Expand All @@ -12,42 +12,27 @@

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

# On each node, replace the 'attributes' key names with the standardized names.
if "attributes" in vdom_tree:
vdom_tree["attributes"] = {_normalize_prop_name(k): v for k, v in vdom_tree["attributes"].items()}

for child in vdom_tree.get("children", []):
convert_html_props_to_reactjs(child)

return vdom_tree


def convert_textarea_children_to_prop(vdom_tree: VdomDict) -> VdomDict:
"""Transformation that converts the text content of a <textarea> to the 'value' prop."""
if not isinstance(vdom_tree, dict):
return vdom_tree

# On each node, if the 'tagName' is 'textarea', move the text content to the 'value' prop.
# If the current tag is <textarea>, move the text content to the 'value' prop.
if vdom_tree["tagName"] == "textarea" and "children" in vdom_tree and vdom_tree["children"]:
text_content = vdom_tree.pop("children")
text_content = "".join([child for child in text_content if isinstance(child, str)])
default_value = vdom_tree["attributes"].pop("defaultValue", "")
vdom_tree["attributes"]["defaultValue"] = text_content or default_value

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

return vdom_tree


def set_value_prop_on_select_element(vdom_tree: VdomDict) -> VdomDict:
"""Use the `value` prop on <select> instead of setting `selected` on <option>."""
if not isinstance(vdom_tree, dict):
return vdom_tree

# If the current tag is <select>, remove 'selected' prop from any <option> children and
# instead set the 'value' prop on the <select> tag.
if vdom_tree["tagName"] == "select" and "children" in vdom_tree:
Expand All @@ -58,48 +43,33 @@ def set_value_prop_on_select_element(vdom_tree: VdomDict) -> VdomDict:
if selected_options and multiple_choice:
vdom_tree["attributes"]["defaultValue"] = selected_options

for child in vdom_tree.get("children", []):
set_value_prop_on_select_element(child)

return vdom_tree


def ensure_input_elements_are_controlled(vdom_tree: VdomDict) -> VdomDict:
"""Adds an onChange handler on form <input> elements, since ReactJS doesn't like uncontrolled inputs."""
if not isinstance(vdom_tree, dict):
return vdom_tree

vdom_tree.setdefault("eventHandlers", {})
if vdom_tree["tagName"] == "input" and "onChange" not in vdom_tree["eventHandlers"]:
vdom_tree["eventHandlers"]["onChange"] = EventHandler(to_event_handler_function(_do_nothing_event))

if "children" in vdom_tree:
for child in vdom_tree["children"]:
ensure_input_elements_are_controlled(child)

return vdom_tree


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

if vdom_tree["tagName"] == "a":
vdom_tree.setdefault("eventHandlers", {})
vdom_tree["eventHandlers"]["onClick"] = EventHandler(
to_event_handler_function(_do_nothing_event), prevent_default=True
)

for child in vdom_tree.get("children", []):
intercept_anchor_links(child)

return vdom_tree


def _find_selected_options(vdom_tree: VdomDict) -> list[str]:
"""Recursively iterate through the tree of dictionaries to find an <option> with the 'selected' prop."""
def _find_selected_options(vdom_tree: VdomDict | Any) -> list[str]:
"""Recursively iterate through the tree to find all <option> tags with the 'selected' prop.
Removes the 'selected' prop and returns a list of the 'value' prop of each selected <option>."""
if not isinstance(vdom_tree, dict):
return []

Expand All @@ -122,8 +92,8 @@ def _normalize_prop_name(prop_name: str) -> str:
return REACT_PROP_SUBSTITUTIONS.get(prop_name, prop_name)


def _react_props_set(string: str) -> set[str]:
"""Extracts the props from a string of React props."""
def _parse_react_props(string: str) -> set[str]:
"""Extracts the props from a string of React props (copy-pasted from ReactJS docs)."""
lines = string.strip().split("\n")
props = set()

Expand All @@ -138,34 +108,12 @@ def _react_props_set(string: str) -> set[str]:
return props


def _add_on_change_event(event_func, vdom_tree: VdomDict) -> VdomDict:
"""Recursively adds an onChange event handler to all elements in the tree."""
if not isinstance(vdom_tree, dict):
return vdom_tree

vdom_tree.setdefault("eventHandlers", {})
if vdom_tree["tagName"] in {"input", "textarea"}:
if "onChange" in vdom_tree["eventHandlers"]:
pass
elif isinstance(event_func, EventHandler):
vdom_tree["eventHandlers"]["onChange"] = event_func
else:
vdom_tree["eventHandlers"]["onChange"] = EventHandler(to_event_handler_function(event_func))

if "children" in vdom_tree:
for child in vdom_tree["children"]:
_add_on_change_event(event_func, child)

return vdom_tree


def _do_nothing_event(*args, **kwargs):
pass
"""A placeholder event function that does nothing."""


# TODO: Create some kind of script that will generate this during build time, rather than hardcoding it.
# TODO: Create some kind of script that will generate the react docs copy-pasta during build time, rather than hardcoding it.
# https://react.dev/reference/react-dom/components/common#common-props

SPECIAL_PROPS = r"""
children: A React node (an element, a string, a number, a portal, an empty node like null, undefined and booleans, or an array of other React nodes). Specifies the content inside the component. When you use JSX, you will usually specify the children prop implicitly by nesting tags like <div><span /></div>.
dangerouslySetInnerHTML: An object of the form { __html: '<p>some html</p>' } with a raw HTML string inside. Overrides the innerHTML property of the DOM node and displays the passed HTML inside. This should be used with extreme caution! If the HTML inside isn't trusted (for example, if it's based on user data), you risk introducing an XSS vulnerability. Read more about using dangerouslySetInnerHTML.
Expand Down Expand Up @@ -496,7 +444,7 @@ def _do_nothing_event(*args, **kwargs):
type: a string. Says whether the script is a classic script, ES module, or import map.
"""

KNOWN_REACT_PROPS = _react_props_set(
KNOWN_REACT_PROPS = _parse_react_props(
SPECIAL_PROPS
+ STANDARD_PROPS
+ FORM_PROPS
Expand Down