Skip to content

Commit

Permalink
Self review: Use JS component as background listener for link clicks
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger committed Oct 12, 2024
1 parent 1d33c65 commit 25e440f
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 50 deletions.
39 changes: 37 additions & 2 deletions src/js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,53 @@ export function bind(node) {
};
}

export function History({ onChange }) {
export function History({ onBrowserBack }) {
// Capture browser "history go back" action and tell the server about it
// Note: Browsers do not allow you to detect "history go forward" actions.
React.useEffect(() => {
// Register a listener for the "popstate" event and send data back to the server using the `onBrowserBack` callback.
const listener = () => {
onChange({
onBrowserBack({
pathname: window.location.pathname,
search: window.location.search,
});
};

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

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

export function Link({ onClick, linkClass }) {
// This component is not the actual anchor link.
// It is an event listener for the link component created by ReactPy.
React.useEffect(() => {
// Event function that will tell the server about clicks
const handleClick = (event) => {
event.preventDefault();
let to = event.target.getAttribute("href");
window.history.pushState({}, to, new URL(to, window.location));
onClick({
pathname: window.location.pathname,
search: window.location.search,
});
};

// Register the event listener
document
.querySelector(`.${linkClass}`)
.addEventListener("click", handleClick);

// Delete the event listener when the component is unmounted
return () => {
document
.querySelector(`.${linkClass}`)
.removeEventListener("click", handleClick);
};
});
return null;
}
59 changes: 22 additions & 37 deletions src/reactpy_router/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

from pathlib import Path
from typing import Any
from urllib.parse import urljoin
from uuid import uuid4

from reactpy import component, event, html, use_connection
from reactpy import component, html
from reactpy.backend.types import Location
from reactpy.core.types import VdomChild, VdomDict
from reactpy.core.types import VdomDict
from reactpy.web.module import export, module_from_file

from reactpy_router.hooks import _use_route_state
Expand All @@ -17,57 +16,43 @@
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
("History"),
)
link_js_content = (Path(__file__).parent / "static" / "link.js").read_text(encoding="utf-8")
Link = export(
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
("Link"),
)


@component
def link(*children: VdomChild, to: str, **attributes: Any) -> VdomDict:
def link(*attributes_and_children: Any, to: str | None = None, **kwargs: Any) -> VdomDict:
"""A component that renders a link to the given path."""
# FIXME: This currently works in a "dumb" way by trusting that ReactPy's script tag \
# properly sets the location. When a client-server communication layer is added to a \
# future ReactPy release, this component will need to be rewritten to use that instead. \
set_location = _use_route_state().set_location
current_path = use_connection().location.pathname

@event(prevent_default=True)
def on_click(_event: dict[str, Any]) -> None:
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))
if to is None:
raise ValueError("The `to` attribute is required for the `Link` component.")

uuid_string = f"link-{uuid4().hex}"
class_name = f"{uuid_string}"
set_location = _use_route_state().set_location
attributes = {}
children: tuple[Any] = attributes_and_children

if attributes_and_children and isinstance(attributes_and_children[0], dict):
attributes = attributes_and_children[0]
children = attributes_and_children[1:]
if "className" in attributes:
class_name = " ".join([attributes.pop("className"), class_name])
# TODO: This can be removed when ReactPy stops supporting underscores in attribute names
if "class_name" in attributes: # pragma: no cover
# TODO: This can be removed when ReactPy stops supporting underscores in attribute names
class_name = " ".join([attributes.pop("class_name"), class_name])

attrs = {
**attributes,
"href": to,
"onClick": on_click,
"className": class_name,
}
return html._(html.a(attrs, *children), html.script(link_js_content.replace("UUID", uuid_string)))

def on_click(_event: dict[str, Any]) -> None:
set_location(Location(**_event))

return html._(html.a(attrs, *children, **kwargs), Link({"onClick": on_click, "linkClass": uuid_string}))


def route(path: str, element: Any | None, *routes: Route) -> Route:
Expand Down
9 changes: 6 additions & 3 deletions src/reactpy_router/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ def router(
)
for element, params in match
]

def on_browser_back(event: dict[str, Any]) -> None:
"""Callback function used within the JavaScript `History` component."""
set_location(Location(**event))

return ConnectionContext(
History( # type: ignore
{"onChange": lambda event: set_location(Location(**event))}
),
History({"onBrowserBack": on_browser_back}), # type: ignore[return-value]
html._(route_elements),
value=Connection(old_conn.scope, location, old_conn.carrier),
)
Expand Down
8 changes: 0 additions & 8 deletions src/reactpy_router/static/link.js

This file was deleted.

0 comments on commit 25e440f

Please sign in to comment.