Skip to content

Commit 464b4e2

Browse files
authored
Refactoring for v5.2 (#273)
1 parent 5596d8d commit 464b4e2

33 files changed

+683
-524
lines changed

.github/workflows/test-python.yml

+15
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,18 @@ jobs:
4646
run: pip install --upgrade pip hatch uv
4747
- name: Check Python formatting
4848
run: hatch fmt src tests --check
49+
50+
python-types:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v4
54+
- uses: oven-sh/setup-bun@v2
55+
with:
56+
bun-version: latest
57+
- uses: actions/setup-python@v5
58+
with:
59+
python-version: 3.x
60+
- name: Install Python Dependencies
61+
run: pip install --upgrade pip hatch uv
62+
- name: Check Python formatting
63+
run: hatch run python:type_check

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Don't forget to remove deprecated code on each major release!
2323

2424
- Automatically convert Django forms to ReactPy forms via the new `reactpy_django.components.django_form` component!
2525

26+
### Changed
27+
28+
- Refactoring of internal code to improve maintainability. No changes to public/documented API.
29+
2630
## [5.1.1] - 2024-12-02
2731

2832
### Fixed
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from django.db import models
2+
3+
4+
class TodoItem(models.Model): ...

docs/examples/python/pyscript_ffi.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from pyscript import document, window
2+
from reactpy import component, html
3+
4+
5+
@component
6+
def root():
7+
def on_click(event):
8+
my_element = document.querySelector("#example")
9+
my_element.innerText = window.location.hostname
10+
11+
return html.div(
12+
{"id": "example"},
13+
html.button({"onClick": on_click}, "Click Me!"),
14+
)

docs/src/about/contributing.md

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ By utilizing `hatch`, the following commands are available to manage the develop
6262
| `hatch fmt --formatter` | Run only formatters |
6363
| `hatch run javascript:check` | Run the JavaScript linter/formatter |
6464
| `hatch run javascript:fix` | Run the JavaScript linter/formatter and write fixes to disk |
65+
| `hatch run python:type_check` | Run the Python type checker |
6566

6667
??? tip "Configure your IDE for linting"
6768

docs/src/reference/template-tag.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,17 @@ The entire file path provided is loaded directly into the browser, and must have
214214
{% include "../../examples/python/pyodide_js_module.py" %}
215215
```
216216

217-
**PyScript FFI**
217+
**PyScript Foreign Function Interface (FFI)**
218218

219-
...
219+
PyScript FFI has similar functionality to Pyodide's `js` module, but utilizes a different API.
220+
221+
There are two importable modules available that are available within the FFI interface: `window` and `document`.
222+
223+
=== "root.py"
224+
225+
```python
226+
{% include "../../examples/python/pyscript_ffi.py" %}
227+
```
220228

221229
**PyScript JS Modules**
222230

pyproject.toml

+13-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ installer = "uv"
7575
[[tool.hatch.build.hooks.build-scripts.scripts]]
7676
commands = [
7777
"bun install --cwd src/js",
78-
"bun build src/js/src/index.tsx --outfile src/reactpy_django/static/reactpy_django/client.js --minify",
78+
"bun build src/js/src/index.ts --outfile src/reactpy_django/static/reactpy_django/client.js --minify",
7979
'cd src/build_scripts && python copy_dir.py "src/js/node_modules/@pyscript/core/dist" "src/reactpy_django/static/reactpy_django/pyscript"',
8080
'cd src/build_scripts && python copy_dir.py "src/js/node_modules/morphdom/dist" "src/reactpy_django/static/reactpy_django/morphdom"',
8181
]
@@ -95,6 +95,8 @@ extra-dependencies = [
9595
"tblib",
9696
"servestatic",
9797
"django-bootstrap5",
98+
"decorator",
99+
98100
]
99101
matrix-name-format = "{variable}-{value}"
100102

@@ -185,6 +187,16 @@ linkcheck = [
185187
deploy_latest = ["cd docs && mike deploy --push --update-aliases {args} latest"]
186188
deploy_develop = ["cd docs && mike deploy --push develop"]
187189

190+
################################
191+
# >>> Hatch Python Scripts <<< #
192+
################################
193+
194+
[tool.hatch.envs.python]
195+
extra-dependencies = ["django-stubs", "channels-redis", "pyright"]
196+
197+
[tool.hatch.envs.python.scripts]
198+
type_check = ["pyright src"]
199+
188200
############################
189201
# >>> Hatch JS Scripts <<< #
190202
############################

src/js/src/client.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ export class ReactPyDjangoClient
3030
this.prerenderElement.remove();
3131
this.prerenderElement = null;
3232
}
33-
if (this.offlineElement) {
33+
if (this.offlineElement && this.mountElement) {
3434
this.mountElement.hidden = true;
3535
this.offlineElement.hidden = false;
3636
}
3737
},
3838
onOpen: () => {
3939
// If offlineElement exists, hide it and show the mountElement
40-
if (this.offlineElement) {
40+
if (this.offlineElement && this.mountElement) {
4141
this.offlineElement.hidden = true;
4242
this.mountElement.hidden = false;
4343
}

src/js/src/components.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { DjangoFormProps } from "./types";
2+
import React from "react";
3+
import ReactDOM from "react-dom";
4+
/**
5+
* Interface used to bind a ReactPy node to React.
6+
*/
7+
export function bind(node) {
8+
return {
9+
create: (type, props, children) =>
10+
React.createElement(type, props, ...children),
11+
render: (element) => {
12+
ReactDOM.render(element, node);
13+
},
14+
unmount: () => ReactDOM.unmountComponentAtNode(node),
15+
};
16+
}
17+
18+
export function DjangoForm({
19+
onSubmitCallback,
20+
formId,
21+
}: DjangoFormProps): null {
22+
React.useEffect(() => {
23+
const form = document.getElementById(formId) as HTMLFormElement;
24+
25+
// Submission event function
26+
const onSubmitEvent = (event) => {
27+
event.preventDefault();
28+
const formData = new FormData(form);
29+
30+
// Convert the FormData object to a plain object by iterating through it
31+
// If duplicate keys are present, convert the value into an array of values
32+
const entries = formData.entries();
33+
const formDataArray = Array.from(entries);
34+
const formDataObject = formDataArray.reduce((acc, [key, value]) => {
35+
if (acc[key]) {
36+
if (Array.isArray(acc[key])) {
37+
acc[key].push(value);
38+
} else {
39+
acc[key] = [acc[key], value];
40+
}
41+
} else {
42+
acc[key] = value;
43+
}
44+
return acc;
45+
}, {});
46+
47+
onSubmitCallback(formDataObject);
48+
};
49+
50+
// Bind the event listener
51+
if (form) {
52+
form.addEventListener("submit", onSubmitEvent);
53+
}
54+
55+
// Unbind the event listener when the component dismounts
56+
return () => {
57+
if (form) {
58+
form.removeEventListener("submit", onSubmitEvent);
59+
}
60+
};
61+
}, []);
62+
63+
return null;
64+
}

src/js/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { DjangoForm, bind } from "./components";
2+
export { mountComponent } from "./mount";

src/js/src/index.tsx renamed to src/js/src/mount.tsx

+1-64
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,6 @@ import { ReactPyDjangoClient } from "./client";
22
import React from "react";
33
import ReactDOM from "react-dom";
44
import { Layout } from "@reactpy/client/src/components";
5-
import { DjangoFormProps } from "./types";
6-
7-
/**
8-
* Interface used to bind a ReactPy node to React.
9-
*/
10-
export function bind(node) {
11-
return {
12-
create: (type, props, children) =>
13-
React.createElement(type, props, ...children),
14-
render: (element) => {
15-
ReactDOM.render(element, node);
16-
},
17-
unmount: () => ReactDOM.unmountComponentAtNode(node),
18-
};
19-
}
205

216
export function mountComponent(
227
mountElement: HTMLElement,
@@ -84,7 +69,7 @@ export function mountComponent(
8469
// Replace the prerender element with the real element on the first layout update
8570
if (client.prerenderElement) {
8671
client.onMessage("layout-update", ({ path, model }) => {
87-
if (client.prerenderElement) {
72+
if (client.prerenderElement && client.mountElement) {
8873
client.prerenderElement.replaceWith(client.mountElement);
8974
client.prerenderElement = null;
9075
}
@@ -94,51 +79,3 @@ export function mountComponent(
9479
// Start rendering the component
9580
ReactDOM.render(<Layout client={client} />, client.mountElement);
9681
}
97-
98-
export function DjangoForm({
99-
onSubmitCallback,
100-
formId,
101-
}: DjangoFormProps): null {
102-
React.useEffect(() => {
103-
const form = document.getElementById(formId) as HTMLFormElement;
104-
105-
// Submission event function
106-
const onSubmitEvent = (event) => {
107-
event.preventDefault();
108-
const formData = new FormData(form);
109-
110-
// Convert the FormData object to a plain object by iterating through it
111-
// If duplicate keys are present, convert the value into an array of values
112-
const entries = formData.entries();
113-
const formDataArray = Array.from(entries);
114-
const formDataObject = formDataArray.reduce((acc, [key, value]) => {
115-
if (acc[key]) {
116-
if (Array.isArray(acc[key])) {
117-
acc[key].push(value);
118-
} else {
119-
acc[key] = [acc[key], value];
120-
}
121-
} else {
122-
acc[key] = value;
123-
}
124-
return acc;
125-
}, {});
126-
127-
onSubmitCallback(formDataObject);
128-
};
129-
130-
// Bind the event listener
131-
if (form) {
132-
form.addEventListener("submit", onSubmitEvent);
133-
}
134-
135-
// Unbind the event listener when the component dismounts
136-
return () => {
137-
if (form) {
138-
form.removeEventListener("submit", onSubmitEvent);
139-
}
140-
};
141-
}, []);
142-
143-
return null;
144-
}

0 commit comments

Comments
 (0)