Skip to content

Refactoring related to ReactPy v1.1.0 #50

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 7 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
Loading