From d5dd01379dd638c082dfa4facdf88335a5c2c7d6 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:52:49 -0800
Subject: [PATCH 1/7] Add type checking
---
.github/workflows/test-python.yml | 15 +++++++++++++++
docs/src/about/contributing.md | 1 +
src/reactpy_router/routers.py | 18 +++++++++---------
src/reactpy_router/types.py | 6 +++---
4 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml
index 9390316..bbac572 100644
--- a/.github/workflows/test-python.yml
+++ b/.github/workflows/test-python.yml
@@ -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
diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md
index c7cf012..82b82f1 100644
--- a/docs/src/about/contributing.md
+++ b/docs/src/about/contributing.md
@@ -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"
diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py
index d8e75f2..c8ac35a 100644
--- a/src/reactpy_router/routers.py
+++ b/src/reactpy_router/routers.py
@@ -7,8 +7,8 @@
from typing import TYPE_CHECKING, Any, Literal, cast
from reactpy import component, use_memo, use_state
-from reactpy.backend.hooks import ConnectionContext, use_connection
from reactpy.backend.types import Connection, Location
+from reactpy.core.hooks import ConnectionContext, use_connection
from reactpy.types import ComponentType, VdomDict
from reactpy_router.components import FirstLoad, History
@@ -20,16 +20,16 @@
from reactpy.core.component import Component
- from reactpy_router.types import CompiledRoute, Resolver, Router, RouteType
+ from reactpy_router.types import CompiledRoute, Resolver, Route, Router
__all__ = ["browser_router", "create_router"]
_logger = getLogger(__name__)
-def create_router(resolver: Resolver[RouteType]) -> Router[RouteType]:
+def create_router(resolver: Resolver[Route]) -> Router[Route]:
"""A decorator that turns a resolver into a router"""
- def wrapper(*routes: RouteType) -> Component:
+ def wrapper(*routes: Route) -> Component:
return router(*routes, resolver=resolver)
return wrapper
@@ -38,13 +38,13 @@ def wrapper(*routes: RouteType) -> Component:
_starlette_router = create_router(StarletteResolver)
-def browser_router(*routes: RouteType) -> Component:
+def browser_router(*routes: Route) -> Component:
"""This is the recommended router for all ReactPy-Router web projects.
It uses the JavaScript [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API)
to manage the history stack.
Args:
- *routes (RouteType): A list of routes to be rendered by the router.
+ *routes (Route): A list of routes to be rendered by the router.
Returns:
A router component that renders the given routes.
@@ -54,8 +54,8 @@ def browser_router(*routes: RouteType) -> Component:
@component
def router(
- *routes: RouteType,
- resolver: Resolver[RouteType],
+ *routes: Route,
+ resolver: Resolver[Route],
) -> VdomDict | None:
"""A component that renders matching route(s) using the given resolver.
@@ -110,7 +110,7 @@ def on_first_load(event: dict[str, Any]) -> None:
return None
-def _iter_routes(routes: Sequence[RouteType]) -> Iterator[RouteType]:
+def _iter_routes(routes: Sequence[Route]) -> Iterator[Route]:
for parent in routes:
for child in _iter_routes(parent.routes):
yield replace(child, path=parent.path + child.path) # type: ignore[misc]
diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py
index 81404b7..ca2c913 100644
--- a/src/reactpy_router/types.py
+++ b/src/reactpy_router/types.py
@@ -46,9 +46,6 @@ def __hash__(self) -> int:
return hash((self.path, key, self.routes))
-RouteType = TypeVar("RouteType", bound=Route)
-"""A type variable for `Route`."""
-
RouteType_contra = TypeVar("RouteType_contra", bound=Route, contravariant=True)
"""A contravariant type variable for `Route`."""
@@ -66,6 +63,7 @@ def __call__(self, *routes: RouteType_contra) -> Component:
Returns:
The resulting component after processing the routes.
"""
+ ...
class Resolver(Protocol[RouteType_contra]):
@@ -81,6 +79,7 @@ def __call__(self, route: RouteType_contra) -> CompiledRoute:
Returns:
The compiled route.
"""
+ ...
class CompiledRoute(Protocol):
@@ -104,6 +103,7 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
Returns:
A tuple containing the associated element and a dictionary of path parameters, or None if the path cannot be resolved.
"""
+ ...
class ConversionInfo(TypedDict):
From 0a460a8dbc6359246b0547e8b000155087b59c3a Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:52:57 -0800
Subject: [PATCH 2/7] Fix readme link
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 4fcafc9..24ad465 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# ReactPy Router
-
-
+
+
From a03b7d3d4049d9274f40af1adf21ea7f1f89b44a Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:53:34 -0800
Subject: [PATCH 3/7] Bump ReactPy minimum version
---
CHANGELOG.md | 24 +++++-------------------
pyproject.toml | 12 +++++++++++-
2 files changed, 16 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6658cea..0a4d30c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,25 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+Don't forget to remove deprecated code on each major release!
+-->
@@ -36,7 +21,8 @@ Using the following categories, list your changes in this order:
### 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`.
### Fixed
diff --git a/pyproject.toml b/pyproject.toml
index 6472bdf..9f4fe96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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/"
@@ -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 <<< #
############################
From 26655e56258090fe42072a2a72c7f0148bee02bb Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 22:22:56 -0800
Subject: [PATCH 4/7] Remove FirstLoad and handle URL mutations on client-side
---
src/js/src/index.ts | 56 ++++++++-----------------------
src/js/src/types.ts | 4 ---
src/js/src/utils.ts | 12 +++++--
src/reactpy_router/components.py | 53 +++--------------------------
src/reactpy_router/routers.py | 13 ++-----
src/reactpy_router/static/link.js | 17 ----------
6 files changed, 32 insertions(+), 123 deletions(-)
delete mode 100644 src/reactpy_router/static/link.js
diff --git a/src/js/src/index.ts b/src/js/src/index.ts
index d7c6b3e..4712637 100644
--- a/src/js/src/index.ts
+++ b/src/js/src/index.ts
@@ -1,12 +1,7 @@
import React from "preact/compat";
import ReactDOM from "preact/compat";
import { createLocationObject, pushState, replaceState } from "./utils";
-import {
- HistoryProps,
- LinkProps,
- NavigateProps,
- FirstLoadProps,
-} from "./types";
+import { HistoryProps, LinkProps, NavigateProps } from "./types";
/**
* Interface used to bind a ReactPy node to React.
@@ -26,8 +21,8 @@ export function bind(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(() => {
- // Register a listener for the "popstate" event and send data back to the server using the `onHistoryChange` callback.
const listener = () => {
onHistoryChangeCallback(createLocationObject());
};
@@ -40,16 +35,10 @@ export function History({ onHistoryChangeCallback }: HistoryProps): null {
});
// 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 () => {};
- // }, []);
+ React.useEffect(() => {
+ onHistoryChangeCallback(createLocationObject());
+ return () => {};
+ }, []);
return null;
}
@@ -58,18 +47,18 @@ export function History({ onHistoryChangeCallback }: HistoryProps): null {
*
* This component is not the actual `` 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());
+ 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
@@ -82,7 +71,6 @@ export function Link({ onClickCallback, linkClass }: LinkProps): null {
// Delete the event listener when the component is unmounted
return () => {
- let link = document.querySelector(`.${linkClass}`);
if (link) {
link.removeEventListener("click", handleClick);
}
@@ -111,19 +99,3 @@ export function Navigate({
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;
-}
diff --git a/src/js/src/types.ts b/src/js/src/types.ts
index f4cf6cd..7144668 100644
--- a/src/js/src/types.ts
+++ b/src/js/src/types.ts
@@ -17,7 +17,3 @@ export interface NavigateProps {
to: string;
replace?: boolean;
}
-
-export interface FirstLoadProps {
- onFirstLoadCallback: (location: ReactPyLocation) => void;
-}
diff --git a/src/js/src/utils.ts b/src/js/src/utils.ts
index a0d1af7..e3f1dd5 100644
--- a/src/js/src/utils.ts
+++ b/src/js/src/utils.ts
@@ -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));
}
diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py
index 6a84799..6a751e7 100644
--- a/src/reactpy_router/components.py
+++ b/src/reactpy_router/components.py
@@ -2,10 +2,9 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any
-from urllib.parse import urljoin
from uuid import uuid4
-from reactpy import component, html, use_connection
+from reactpy import component, html, use_connection, use_ref
from reactpy.backend.types import Location
from reactpy.web.module import export, module_from_file
@@ -34,13 +33,6 @@
)
"""Client-side portion of the navigate component"""
-FirstLoad = export(
- module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
- ("FirstLoad"),
-)
-
-link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8")
-
def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) -> Component:
"""
@@ -59,8 +51,7 @@ def link(attributes: dict[str, Any], *children: Any, key: Key | None = None) ->
@component
def _link(attributes: dict[str, Any], *children: Any) -> VdomDict:
attributes = attributes.copy()
- uuid_string = f"link-{uuid4().hex}"
- class_name = f"{uuid_string}"
+ class_name = use_ref(f"link-{uuid4().hex}").current
set_location = _use_route_state().set_location
if "className" in attributes:
class_name = " ".join([attributes.pop("className"), class_name])
@@ -80,44 +71,10 @@ def _link(attributes: dict[str, Any], *children: Any) -> VdomDict:
"className": class_name,
}
- # FIXME: This component currently works in a "dumb" way by trusting that ReactPy's script tag \
- # properly sets the location due to bugs in ReactPy rendering.
- # https://github.com/reactive-python/reactpy/pull/1224
- current_path = use_connection().location.pathname
-
- def on_click(_event: dict[str, Any]) -> None:
- if _event.get("ctrlKey", False):
- return
-
- pathname, search = to.split("?", 1) if "?" in to else (to, "")
- if search:
- search = f"?{search}"
-
- # Resolve relative paths that match `../foo`
- if pathname.startswith("../"):
- pathname = urljoin(current_path, pathname)
-
- # Resolve relative paths that match `foo`
- if not pathname.startswith("/"):
- pathname = urljoin(current_path, pathname)
-
- # Resolve relative paths that match `/foo/../bar`
- while "/../" in pathname:
- part_1, part_2 = pathname.split("/../", 1)
- pathname = urljoin(f"{part_1}/", f"../{part_2}")
-
- # Resolve relative paths that match `foo/./bar`
- pathname = pathname.replace("/./", "/")
-
- set_location(Location(pathname, search))
-
- attrs["onClick"] = on_click
-
- return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string)))
+ def on_click_callback(_event: dict[str, Any]) -> None:
+ set_location(Location(**_event))
- # def on_click_callback(_event: dict[str, Any]) -> None:
- # set_location(Location(**_event))
- # return html._(html.a(attrs, *children), Link({"onClickCallback": on_click_callback, "linkClass": uuid_string}))
+ return html._(Link({"onClickCallback": on_click_callback, "linkClass": class_name}), html.a(attrs, *children))
def route(path: str, element: Any | None, *routes: Route) -> Route:
diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py
index c8ac35a..25c37b4 100644
--- a/src/reactpy_router/routers.py
+++ b/src/reactpy_router/routers.py
@@ -11,7 +11,7 @@
from reactpy.core.hooks import ConnectionContext, use_connection
from reactpy.types import ComponentType, VdomDict
-from reactpy_router.components import FirstLoad, History
+from reactpy_router.components import History
from reactpy_router.hooks import RouteState, _route_state_context
from reactpy_router.resolvers import StarletteResolver
@@ -76,9 +76,9 @@ def router(
if match:
if first_load:
# We need skip rendering the application on 'first_load' to avoid
- # rendering it twice. The second render occurs following
- # the impending on_history_change event
+ # rendering it twice. The second render follows the on_history_change event
route_elements = []
+ set_first_load(False)
else:
route_elements = [
_route_state_context(
@@ -94,15 +94,8 @@ def on_history_change(event: dict[str, Any]) -> None:
if location != new_location:
set_location(new_location)
- def on_first_load(event: dict[str, Any]) -> None:
- """Callback function used within the JavaScript `FirstLoad` component."""
- if first_load:
- set_first_load(False)
- on_history_change(event)
-
return ConnectionContext(
History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value]
- FirstLoad({"onFirstLoadCallback": on_first_load}) if first_load else "",
*route_elements,
value=Connection(old_conn.scope, location, old_conn.carrier),
)
diff --git a/src/reactpy_router/static/link.js b/src/reactpy_router/static/link.js
deleted file mode 100644
index 7ab069b..0000000
--- a/src/reactpy_router/static/link.js
+++ /dev/null
@@ -1,17 +0,0 @@
-document.querySelector(".UUID").addEventListener(
- "click",
- (event) => {
- // Prevent default if ctrl isn't pressed
- if (!event.ctrlKey) {
- event.preventDefault();
- let to = event.currentTarget.getAttribute("href");
- let new_url = new URL(to, window.location);
-
- // Deduplication needed due to ReactPy rendering bug
- if (new_url.href !== window.location.href) {
- window.history.pushState(null, "", new URL(to, window.location));
- }
- }
- },
- { once: true },
-);
From 2b3e93f68e50f858d5652a7b0b6721e5c0fcbc2f Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 22:25:39 -0800
Subject: [PATCH 5/7] fix bun file path
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 9f4fe96..0f3d7f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 = []
From 19a707636ffe4f0a92b05a4c58cd94cb83f552ec Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 22:29:18 -0800
Subject: [PATCH 6/7] Add changelog
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a4d30c..1fced87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,8 @@ Don't forget to remove deprecated code on each major release!
- 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
From c8e92c66834325f6eadac5243c8a917ef27919eb Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 11 Jan 2025 22:35:59 -0800
Subject: [PATCH 7/7] Move components to their own file
---
src/js/src/components.ts | 101 ++++++++++++++++++++++++++++++++++++++
src/js/src/index.ts | 102 +--------------------------------------
2 files changed, 102 insertions(+), 101 deletions(-)
create mode 100644 src/js/src/components.ts
diff --git a/src/js/src/components.ts b/src/js/src/components.ts
new file mode 100644
index 0000000..4712637
--- /dev/null
+++ b/src/js/src/components.ts
@@ -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 `` 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;
+}
diff --git a/src/js/src/index.ts b/src/js/src/index.ts
index 4712637..8de0626 100644
--- a/src/js/src/index.ts
+++ b/src/js/src/index.ts
@@ -1,101 +1 @@
-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 `` 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;
-}
+export { bind, History, Link, Navigate } from "./components";