Skip to content

Commit

Permalink
Refactoring for v5.2 (#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger authored Dec 19, 2024
1 parent 5596d8d commit 464b4e2
Show file tree
Hide file tree
Showing 33 changed files with 683 additions and 524 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,18 @@ jobs:
run: pip install --upgrade pip hatch uv
- name: Check Python formatting
run: hatch fmt src tests --check

python-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Install Python Dependencies
run: pip install --upgrade pip hatch uv
- name: Check Python formatting
run: hatch run python:type_check
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Don't forget to remove deprecated code on each major release!

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

### Changed

- Refactoring of internal code to improve maintainability. No changes to public/documented API.

## [5.1.1] - 2024-12-02

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions docs/examples/python/example/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.db import models


class TodoItem(models.Model): ...
14 changes: 14 additions & 0 deletions docs/examples/python/pyscript_ffi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pyscript import document, window
from reactpy import component, html


@component
def root():
def on_click(event):
my_element = document.querySelector("#example")
my_element.innerText = window.location.hostname

return html.div(
{"id": "example"},
html.button({"onClick": on_click}, "Click Me!"),
)
1 change: 1 addition & 0 deletions docs/src/about/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ By utilizing `hatch`, the following commands are available to manage the develop
| `hatch fmt --formatter` | Run only formatters |
| `hatch run javascript:check` | Run the JavaScript linter/formatter |
| `hatch run javascript:fix` | Run the JavaScript linter/formatter and write fixes to disk |
| `hatch run python:type_check` | Run the Python type checker |

??? tip "Configure your IDE for linting"

Expand Down
12 changes: 10 additions & 2 deletions docs/src/reference/template-tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,17 @@ The entire file path provided is loaded directly into the browser, and must have
{% include "../../examples/python/pyodide_js_module.py" %}
```

**PyScript FFI**
**PyScript Foreign Function Interface (FFI)**

...
PyScript FFI has similar functionality to Pyodide's `js` module, but utilizes a different API.

There are two importable modules available that are available within the FFI interface: `window` and `document`.

=== "root.py"

```python
{% include "../../examples/python/pyscript_ffi.py" %}
```

**PyScript JS Modules**

Expand Down
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ installer = "uv"
[[tool.hatch.build.hooks.build-scripts.scripts]]
commands = [
"bun install --cwd src/js",
"bun build src/js/src/index.tsx --outfile src/reactpy_django/static/reactpy_django/client.js --minify",
"bun build src/js/src/index.ts --outfile src/reactpy_django/static/reactpy_django/client.js --minify",
'cd src/build_scripts && python copy_dir.py "src/js/node_modules/@pyscript/core/dist" "src/reactpy_django/static/reactpy_django/pyscript"',
'cd src/build_scripts && python copy_dir.py "src/js/node_modules/morphdom/dist" "src/reactpy_django/static/reactpy_django/morphdom"',
]
Expand All @@ -95,6 +95,8 @@ extra-dependencies = [
"tblib",
"servestatic",
"django-bootstrap5",
"decorator",

]
matrix-name-format = "{variable}-{value}"

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

################################
# >>> Hatch Python Scripts <<< #
################################

[tool.hatch.envs.python]
extra-dependencies = ["django-stubs", "channels-redis", "pyright"]

[tool.hatch.envs.python.scripts]
type_check = ["pyright src"]

############################
# >>> Hatch JS Scripts <<< #
############################
Expand Down
4 changes: 2 additions & 2 deletions src/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ export class ReactPyDjangoClient
this.prerenderElement.remove();
this.prerenderElement = null;
}
if (this.offlineElement) {
if (this.offlineElement && this.mountElement) {
this.mountElement.hidden = true;
this.offlineElement.hidden = false;
}
},
onOpen: () => {
// If offlineElement exists, hide it and show the mountElement
if (this.offlineElement) {
if (this.offlineElement && this.mountElement) {
this.offlineElement.hidden = true;
this.mountElement.hidden = false;
}
Expand Down
64 changes: 64 additions & 0 deletions src/js/src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DjangoFormProps } from "./types";
import React from "react";
import ReactDOM from "react-dom";
/**
* Interface used to bind a ReactPy node to React.
*/
export function bind(node) {
return {
create: (type, props, children) =>
React.createElement(type, props, ...children),
render: (element) => {
ReactDOM.render(element, node);
},
unmount: () => ReactDOM.unmountComponentAtNode(node),
};
}

export function DjangoForm({
onSubmitCallback,
formId,
}: DjangoFormProps): null {
React.useEffect(() => {
const form = document.getElementById(formId) as HTMLFormElement;

// Submission event function
const onSubmitEvent = (event) => {
event.preventDefault();
const formData = new FormData(form);

// Convert the FormData object to a plain object by iterating through it
// If duplicate keys are present, convert the value into an array of values
const entries = formData.entries();
const formDataArray = Array.from(entries);
const formDataObject = formDataArray.reduce((acc, [key, value]) => {
if (acc[key]) {
if (Array.isArray(acc[key])) {
acc[key].push(value);
} else {
acc[key] = [acc[key], value];
}
} else {
acc[key] = value;
}
return acc;
}, {});

onSubmitCallback(formDataObject);
};

// Bind the event listener
if (form) {
form.addEventListener("submit", onSubmitEvent);
}

// Unbind the event listener when the component dismounts
return () => {
if (form) {
form.removeEventListener("submit", onSubmitEvent);
}
};
}, []);

return null;
}
2 changes: 2 additions & 0 deletions src/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DjangoForm, bind } from "./components";
export { mountComponent } from "./mount";
65 changes: 1 addition & 64 deletions src/js/src/index.tsx → src/js/src/mount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,6 @@ import { ReactPyDjangoClient } from "./client";
import React from "react";
import ReactDOM from "react-dom";
import { Layout } from "@reactpy/client/src/components";
import { DjangoFormProps } from "./types";

/**
* Interface used to bind a ReactPy node to React.
*/
export function bind(node) {
return {
create: (type, props, children) =>
React.createElement(type, props, ...children),
render: (element) => {
ReactDOM.render(element, node);
},
unmount: () => ReactDOM.unmountComponentAtNode(node),
};
}

export function mountComponent(
mountElement: HTMLElement,
Expand Down Expand Up @@ -84,7 +69,7 @@ export function mountComponent(
// Replace the prerender element with the real element on the first layout update
if (client.prerenderElement) {
client.onMessage("layout-update", ({ path, model }) => {
if (client.prerenderElement) {
if (client.prerenderElement && client.mountElement) {
client.prerenderElement.replaceWith(client.mountElement);
client.prerenderElement = null;
}
Expand All @@ -94,51 +79,3 @@ export function mountComponent(
// Start rendering the component
ReactDOM.render(<Layout client={client} />, client.mountElement);
}

export function DjangoForm({
onSubmitCallback,
formId,
}: DjangoFormProps): null {
React.useEffect(() => {
const form = document.getElementById(formId) as HTMLFormElement;

// Submission event function
const onSubmitEvent = (event) => {
event.preventDefault();
const formData = new FormData(form);

// Convert the FormData object to a plain object by iterating through it
// If duplicate keys are present, convert the value into an array of values
const entries = formData.entries();
const formDataArray = Array.from(entries);
const formDataObject = formDataArray.reduce((acc, [key, value]) => {
if (acc[key]) {
if (Array.isArray(acc[key])) {
acc[key].push(value);
} else {
acc[key] = [acc[key], value];
}
} else {
acc[key] = value;
}
return acc;
}, {});

onSubmitCallback(formDataObject);
};

// Bind the event listener
if (form) {
form.addEventListener("submit", onSubmitEvent);
}

// Unbind the event listener when the component dismounts
return () => {
if (form) {
form.removeEventListener("submit", onSubmitEvent);
}
};
}, []);

return null;
}
Loading

0 comments on commit 464b4e2

Please sign in to comment.