Skip to content

Refactoring for v5.2 #273

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 28 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cfde802
Use `ensure_async` instead of `database_sync_to_async`
Archmonger Dec 11, 2024
1070b80
Handle all pyright warnings
Archmonger Dec 11, 2024
8ec20b7
Add CI workflow for pyright
Archmonger Dec 11, 2024
3711803
Grouo pyscript related code into module
Archmonger Dec 11, 2024
1ee2799
cached_static_contents -> cached_static_file
Archmonger Dec 11, 2024
e7d1764
vdom_or_component_to_string -> reactpy_to_string
Archmonger Dec 11, 2024
67966e2
strtobool -> str_to_bool
Archmonger Dec 11, 2024
8ab7f23
Fix borked type hints
Archmonger Dec 11, 2024
5d2dd0e
More generic function for form field conversion
Archmonger Dec 11, 2024
16f076d
validate_form_args function
Archmonger Dec 11, 2024
1a4fcaa
Refactor javascript
Archmonger Dec 11, 2024
90a6308
rename `clean` module to `tasks`
Archmonger Dec 11, 2024
0b593a5
Fix one instance of test flakiness
Archmonger Dec 11, 2024
0cd8218
Add test retries
Archmonger Dec 12, 2024
1e1bf36
hatch fmt
Archmonger Dec 12, 2024
2a1fa06
rename hook async_render -> render_view
Archmonger Dec 12, 2024
c8d6cf9
Turn navigate_to_page into a decorator
Archmonger Dec 12, 2024
9f9f99d
Refactor PlaywrightTestCase
Archmonger Dec 13, 2024
c5f8d68
Move flaky marker to sync form test
Archmonger Dec 13, 2024
fa13697
Apparently I forgot PyScript FFI docs
Archmonger Dec 14, 2024
a607daa
Test performance optimization: Use same class for alll component tests
Archmonger Dec 18, 2024
4165542
Fix typo
Archmonger Dec 18, 2024
f99e585
Increase prerender sleepy time
Archmonger Dec 18, 2024
6238383
more sleepy time
Archmonger Dec 19, 2024
207cf19
variable name and docstring cleanup
Archmonger Dec 19, 2024
08fd30c
Add changelog
Archmonger Dec 19, 2024
39b29fc
prevent test pages from jumping around
Archmonger Dec 19, 2024
9d1c65f
More sleepy time on github
Archmonger Dec 19, 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
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
Loading