forked from reactive-python/reactpy-router
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrouters.py
127 lines (93 loc) · 4.13 KB
/
routers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""URL router implementation for ReactPy"""
from __future__ import annotations
from dataclasses import replace
from logging import getLogger
from typing import TYPE_CHECKING, Any, Union, cast
from reactpy import component, use_memo, use_state
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 History
from reactpy_router.hooks import RouteState, _route_state_context
from reactpy_router.resolvers import ReactPyResolver
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from reactpy.core.component import Component
from reactpy_router.types import CompiledRoute, MatchedRoute, Resolver, Route, Router
__all__ = ["browser_router", "create_router"]
_logger = getLogger(__name__)
def create_router(resolver: Resolver[Route]) -> Router[Route]:
"""A decorator that turns a resolver into a router"""
def wrapper(*routes: Route) -> Component:
return router(*routes, resolver=resolver)
return wrapper
_router = create_router(ReactPyResolver)
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 (Route): A list of routes to be rendered by the router.
Returns:
A router component that renders the given routes.
"""
return _router(*routes)
@component
def router(
*routes: Route,
resolver: Resolver[Route],
) -> VdomDict | None:
"""A component that renders matching route using the given resolver.
User notice: This component typically should never be used. Instead, use `create_router` if creating
a custom routing engine."""
old_connection = use_connection()
location, set_location = use_state(cast(Union[Location, None], None))
resolvers = use_memo(
lambda: tuple(map(resolver, _iter_routes(routes))),
dependencies=(resolver, hash(routes)),
)
route_element = None
match = use_memo(lambda: _match_route(resolvers, location or old_connection.location))
if match:
# Skip rendering until ReactPy-Router knows what URL the page is on.
if location:
route_element = _route_state_context(
match.element,
value=RouteState(set_location, match.params),
)
def on_history_change(event: dict[str, Any]) -> None:
"""Callback function used within the JavaScript `History` component."""
new_location = Location(**event)
if location != new_location:
set_location(new_location)
return ConnectionContext(
History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value]
route_element,
value=Connection(old_connection.scope, location or old_connection.location, old_connection.carrier),
)
return None
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]
yield parent
def _add_route_key(match: MatchedRoute, key: str | int) -> Any:
"""Add a key to the VDOM or component on the current route, if it doesn't already have one."""
element = match.element
if hasattr(element, "render") and not element.key:
element = cast(ComponentType, element)
element.key = key
elif isinstance(element, dict) and not element.get("key", None):
element = cast(VdomDict, element)
element["key"] = key
return match
def _match_route(
compiled_routes: Sequence[CompiledRoute],
location: Location,
) -> MatchedRoute | None:
for resolver in compiled_routes:
match = resolver.resolve(location.pathname)
if match is not None:
return _add_route_key(match, resolver.key)
_logger.debug("No matching route found for %s", location.pathname)
return None