Skip to content

Commit

Permalink
Refactoring related to ReactPy v1.1.0 (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger authored Jan 12, 2025
1 parent 44f76d2 commit 2d79831
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 245 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,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: Run Python type checker
run: hatch run python:type_check
26 changes: 7 additions & 19 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

<!--
Using the following categories, list your changes in this order:
[Added, Changed, Deprecated, Removed, Fixed, Security]
### Added
- for new features.
### Changed
- for changes in existing functionality.
### Deprecated
- for soon-to-be removed features.
### Removed
- for removed features.
### Fixed
- for bug fixes.
### Security
- for vulnerability fixes.
-->
Don't forget to remove deprecated code on each major release!
-->

<!--changelog-start-->

## [Unreleased]

### Changed

- Set upper limit on ReactPy version to `<2.0.0`.
- Set maximum ReactPy version to `<2.0.0`.
- Set minimum ReactPy version to `1.1.0`.
- `link` element now calculates URL changes using the client.
- Refactoring related to `reactpy>=1.1.0` changes.

### Fixed

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# <img src="https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg" align="left" height="45"/> ReactPy Router

<p>
<a href="https://github.com/reactive-python/reactpy-router/actions/workflows/test-src.yml">
<img src="https://github.com/reactive-python/reactpy-router/actions/workflows/test-src.yml/badge.svg">
<a href="https://github.com/reactive-python/reactpy-router/actions/workflows/test-python.yml">
<img src="https://github.com/reactive-python/reactpy-router/actions/workflows/test-python.yml/badge.svg">
</a>
<a href="https://pypi.python.org/pypi/reactpy-router">
<img src="https://img.shields.io/pypi/v/reactpy-router.svg?label=PyPI">
Expand Down
1 change: 1 addition & 0 deletions docs/src/about/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
"Environment :: Web Environment",
"Typing :: Typed",
]
dependencies = ["reactpy>=1.0.0, <2.0.0", "typing_extensions"]
dependencies = ["reactpy>=1.1.0, <2.0.0", "typing_extensions"]
dynamic = ["version"]
urls.Changelog = "https://reactive-python.github.io/reactpy-router/latest/about/changelog/"
urls.Documentation = "https://reactive-python.github.io/reactpy-router/latest/"
Expand All @@ -53,7 +53,7 @@ installer = "uv"
[[tool.hatch.build.hooks.build-scripts.scripts]]
commands = [
"bun install --cwd src/js",
"bun build src/js/src/index.js --outfile src/reactpy_router/static/bundle.js --minify",
"bun build src/js/src/index.ts --outfile src/reactpy_router/static/bundle.js --minify",
]
artifacts = []

Expand Down Expand Up @@ -106,6 +106,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 = ["pyright"]

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

############################
# >>> Hatch JS Scripts <<< #
############################
Expand Down
101 changes: 101 additions & 0 deletions src/js/src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from "preact/compat";
import ReactDOM from "preact/compat";
import { createLocationObject, pushState, replaceState } from "./utils";
import { HistoryProps, LinkProps, NavigateProps } 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),
};
}

/**
* History component that captures browser "history go back" actions and notifies the server.
*/
export function History({ onHistoryChangeCallback }: HistoryProps): null {
// Tell the server about history "popstate" events
React.useEffect(() => {
const listener = () => {
onHistoryChangeCallback(createLocationObject());
};

// Register the event listener
window.addEventListener("popstate", listener);

// Delete the event listener when the component is unmounted
return () => window.removeEventListener("popstate", listener);
});

// Tell the server about the URL during the initial page load
React.useEffect(() => {
onHistoryChangeCallback(createLocationObject());
return () => {};
}, []);
return null;
}

/**
* Link component that captures clicks on anchor links and notifies the server.
*
* This component is not the actual `<a>` link element. It is just an event
* listener for ReactPy-Router's server-side link component.
*/
export function Link({ onClickCallback, linkClass }: LinkProps): null {
React.useEffect(() => {
// Event function that will tell the server about clicks
const handleClick = (event: Event) => {
let click_event = event as MouseEvent;
if (!click_event.ctrlKey) {
event.preventDefault();
let to = (event.currentTarget as HTMLElement).getAttribute("href");
pushState(to);
onClickCallback(createLocationObject());
}
};

// Register the event listener
let link = document.querySelector(`.${linkClass}`);
if (link) {
link.addEventListener("click", handleClick);
} else {
console.warn(`Link component with class name ${linkClass} not found.`);
}

// Delete the event listener when the component is unmounted
return () => {
if (link) {
link.removeEventListener("click", handleClick);
}
};
});
return null;
}

/**
* Client-side portion of the navigate component, that allows the server to command the client to change URLs.
*/
export function Navigate({
onNavigateCallback,
to,
replace = false,
}: NavigateProps): null {
React.useEffect(() => {
if (replace) {
replaceState(to);
} else {
pushState(to);
}
onNavigateCallback(createLocationObject());
return () => {};
}, []);

return null;
}
130 changes: 1 addition & 129 deletions src/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,129 +1 @@
import React from "preact/compat";
import ReactDOM from "preact/compat";
import { createLocationObject, pushState, replaceState } from "./utils";
import {
HistoryProps,
LinkProps,
NavigateProps,
FirstLoadProps,
} 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),
};
}

/**
* History component that captures browser "history go back" actions and notifies the server.
*/
export function History({ onHistoryChangeCallback }: HistoryProps): null {
React.useEffect(() => {
// Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback.
const listener = () => {
onHistoryChangeCallback(createLocationObject());
};

// Register the event listener
window.addEventListener("popstate", listener);

// Delete the event listener when the component is unmounted
return () => window.removeEventListener("popstate", listener);
});

// Tell the server about the URL during the initial page load
// FIXME: This code is commented out since it currently runs every time any component
// is mounted due to a ReactPy core rendering bug. `FirstLoad` component is used instead.
// https://github.com/reactive-python/reactpy/pull/1224
// React.useEffect(() => {
// onHistoryChange({
// pathname: window.location.pathname,
// search: window.location.search,
// });
// return () => {};
// }, []);
return null;
}

/**
* Link component that captures clicks on anchor links and notifies the server.
*
* This component is not the actual `<a>` link element. It is just an event
* listener for ReactPy-Router's server-side link component.
*
* @disabled This component is currently unused due to a ReactPy core rendering bug
* which causes duplicate rendering (and thus duplicate event listeners).
*/
export function Link({ onClickCallback, linkClass }: LinkProps): null {
React.useEffect(() => {
// Event function that will tell the server about clicks
const handleClick = (event: MouseEvent) => {
event.preventDefault();
let to = (event.target as HTMLElement).getAttribute("href");
pushState(to);
onClickCallback(createLocationObject());
};

// Register the event listener
let link = document.querySelector(`.${linkClass}`);
if (link) {
link.addEventListener("click", handleClick);
} else {
console.warn(`Link component with class name ${linkClass} not found.`);
}

// Delete the event listener when the component is unmounted
return () => {
let link = document.querySelector(`.${linkClass}`);
if (link) {
link.removeEventListener("click", handleClick);
}
};
});
return null;
}

/**
* Client-side portion of the navigate component, that allows the server to command the client to change URLs.
*/
export function Navigate({
onNavigateCallback,
to,
replace = false,
}: NavigateProps): null {
React.useEffect(() => {
if (replace) {
replaceState(to);
} else {
pushState(to);
}
onNavigateCallback(createLocationObject());
return () => {};
}, []);

return null;
}

/**
* FirstLoad component that captures the URL during the initial page load and notifies the server.
*
* FIXME: This component only exists because of a ReactPy core rendering bug, and should be removed when the bug
* is fixed. In the future, all this logic should be handled by the `History` component.
* https://github.com/reactive-python/reactpy/pull/1224
*/
export function FirstLoad({ onFirstLoadCallback }: FirstLoadProps): null {
React.useEffect(() => {
onFirstLoadCallback(createLocationObject());
return () => {};
}, []);

return null;
}
export { bind, History, Link, Navigate } from "./components";
4 changes: 0 additions & 4 deletions src/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,3 @@ export interface NavigateProps {
to: string;
replace?: boolean;
}

export interface FirstLoadProps {
onFirstLoadCallback: (location: ReactPyLocation) => void;
}
12 changes: 10 additions & 2 deletions src/js/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ export function createLocationObject(): ReactPyLocation {
};
}

export function pushState(to: string): void {
export function pushState(to: any): void {
if (typeof to !== "string") {
console.error("pushState() requires a string argument.");
return;
}
window.history.pushState(null, "", new URL(to, window.location.href));
}

export function replaceState(to: string): void {
export function replaceState(to: any): void {
if (typeof to !== "string") {
console.error("replaceState() requires a string argument.");
return;
}
window.history.replaceState(null, "", new URL(to, window.location.href));
}
Loading

0 comments on commit 2d79831

Please sign in to comment.