Skip to content

Commit 25e440f

Browse files
committed
Self review: Use JS component as background listener for link clicks
1 parent 1d33c65 commit 25e440f

File tree

4 files changed

+65
-50
lines changed

4 files changed

+65
-50
lines changed

src/js/src/index.js

+37-2
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,53 @@ export function bind(node) {
1212
};
1313
}
1414

15-
export function History({ onChange }) {
15+
export function History({ onBrowserBack }) {
1616
// Capture browser "history go back" action and tell the server about it
1717
// Note: Browsers do not allow you to detect "history go forward" actions.
1818
React.useEffect(() => {
19+
// Register a listener for the "popstate" event and send data back to the server using the `onBrowserBack` callback.
1920
const listener = () => {
20-
onChange({
21+
onBrowserBack({
2122
pathname: window.location.pathname,
2223
search: window.location.search,
2324
});
2425
};
26+
27+
// Register the event listener
2528
window.addEventListener("popstate", listener);
29+
30+
// Delete the event listener when the component is unmounted
2631
return () => window.removeEventListener("popstate", listener);
2732
});
2833
return null;
2934
}
35+
36+
export function Link({ onClick, linkClass }) {
37+
// This component is not the actual anchor link.
38+
// It is an event listener for the link component created by ReactPy.
39+
React.useEffect(() => {
40+
// Event function that will tell the server about clicks
41+
const handleClick = (event) => {
42+
event.preventDefault();
43+
let to = event.target.getAttribute("href");
44+
window.history.pushState({}, to, new URL(to, window.location));
45+
onClick({
46+
pathname: window.location.pathname,
47+
search: window.location.search,
48+
});
49+
};
50+
51+
// Register the event listener
52+
document
53+
.querySelector(`.${linkClass}`)
54+
.addEventListener("click", handleClick);
55+
56+
// Delete the event listener when the component is unmounted
57+
return () => {
58+
document
59+
.querySelector(`.${linkClass}`)
60+
.removeEventListener("click", handleClick);
61+
};
62+
});
63+
return null;
64+
}

src/reactpy_router/components.py

+22-37
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
from pathlib import Path
44
from typing import Any
5-
from urllib.parse import urljoin
65
from uuid import uuid4
76

8-
from reactpy import component, event, html, use_connection
7+
from reactpy import component, html
98
from reactpy.backend.types import Location
10-
from reactpy.core.types import VdomChild, VdomDict
9+
from reactpy.core.types import VdomDict
1110
from reactpy.web.module import export, module_from_file
1211

1312
from reactpy_router.hooks import _use_route_state
@@ -17,57 +16,43 @@
1716
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
1817
("History"),
1918
)
20-
link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8")
19+
Link = export(
20+
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
21+
("Link"),
22+
)
2123

2224

2325
@component
24-
def link(*children: VdomChild, to: str, **attributes: Any) -> VdomDict:
26+
def link(*attributes_and_children: Any, to: str | None = None, **kwargs: Any) -> VdomDict:
2527
"""A component that renders a link to the given path."""
26-
# FIXME: This currently works in a "dumb" way by trusting that ReactPy's script tag \
27-
# properly sets the location. When a client-server communication layer is added to a \
28-
# future ReactPy release, this component will need to be rewritten to use that instead. \
29-
set_location = _use_route_state().set_location
30-
current_path = use_connection().location.pathname
31-
32-
@event(prevent_default=True)
33-
def on_click(_event: dict[str, Any]) -> None:
34-
pathname, search = to.split("?", 1) if "?" in to else (to, "")
35-
if search:
36-
search = f"?{search}"
37-
38-
# Resolve relative paths that match `../foo`
39-
if pathname.startswith("../"):
40-
pathname = urljoin(current_path, pathname)
41-
42-
# Resolve relative paths that match `foo`
43-
if not pathname.startswith("/"):
44-
pathname = urljoin(current_path, pathname)
45-
46-
# Resolve relative paths that match `/foo/../bar`
47-
while "/../" in pathname:
48-
part_1, part_2 = pathname.split("/../", 1)
49-
pathname = urljoin(f"{part_1}/", f"../{part_2}")
50-
51-
# Resolve relative paths that match `foo/./bar`
52-
pathname = pathname.replace("/./", "/")
53-
54-
set_location(Location(pathname, search))
28+
if to is None:
29+
raise ValueError("The `to` attribute is required for the `Link` component.")
5530

5631
uuid_string = f"link-{uuid4().hex}"
5732
class_name = f"{uuid_string}"
33+
set_location = _use_route_state().set_location
34+
attributes = {}
35+
children: tuple[Any] = attributes_and_children
36+
37+
if attributes_and_children and isinstance(attributes_and_children[0], dict):
38+
attributes = attributes_and_children[0]
39+
children = attributes_and_children[1:]
5840
if "className" in attributes:
5941
class_name = " ".join([attributes.pop("className"), class_name])
60-
# TODO: This can be removed when ReactPy stops supporting underscores in attribute names
6142
if "class_name" in attributes: # pragma: no cover
43+
# TODO: This can be removed when ReactPy stops supporting underscores in attribute names
6244
class_name = " ".join([attributes.pop("class_name"), class_name])
6345

6446
attrs = {
6547
**attributes,
6648
"href": to,
67-
"onClick": on_click,
6849
"className": class_name,
6950
}
70-
return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string)))
51+
52+
def on_click(_event: dict[str, Any]) -> None:
53+
set_location(Location(**_event))
54+
55+
return html._(html.a(attrs, *children, **kwargs), Link({"onClick": on_click, "linkClass": uuid_string}))
7156

7257

7358
def route(path: str, element: Any | None, *routes: Route) -> Route:

src/reactpy_router/routers.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ def router(
6868
)
6969
for element, params in match
7070
]
71+
72+
def on_browser_back(event: dict[str, Any]) -> None:
73+
"""Callback function used within the JavaScript `History` component."""
74+
set_location(Location(**event))
75+
7176
return ConnectionContext(
72-
History( # type: ignore
73-
{"onChange": lambda event: set_location(Location(**event))}
74-
),
77+
History({"onBrowserBack": on_browser_back}), # type: ignore[return-value]
7578
html._(route_elements),
7679
value=Connection(old_conn.scope, location, old_conn.carrier),
7780
)

src/reactpy_router/static/link.js

-8
This file was deleted.

0 commit comments

Comments
 (0)