-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathrouter.py
129 lines (100 loc) · 3.64 KB
/
router.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
128
129
from __future__ import annotations
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Any, Callable, Iterator, Sequence, TypeVar
from urllib.parse import parse_qs
from idom import (
component,
create_context,
use_context,
use_location,
use_memo,
use_state,
)
from idom.backend.hooks import ConnectionContext, use_connection
from idom.backend.types import Connection, Location
from idom.core.types import VdomChild, VdomDict
from idom.types import ComponentType, Context, Location
from idom.web.module import export, module_from_file
from idom_router.compilers import compile_starlette_route
from idom_router.types import Route, RouteCompiler, RoutePattern
R = TypeVar("R", bound=Route)
@component
def router(
*routes: R,
compiler: RouteCompiler[R] = compile_starlette_route,
) -> ComponentType | None:
old_conn = use_connection()
location, set_location = use_state(old_conn.location)
# Memoize the compiled routes and the match separately so that we don't
# recompile the routes on renders where only the location has changed
compiled_routes = use_memo(lambda: _compile_routes(routes, compiler))
match = use_memo(lambda: _match_route(compiled_routes, location))
if match is not None:
route, params = match
return ConnectionContext(
_route_state_context(
route.element, value=_RouteState(set_location, params)
),
value=Connection(old_conn.scope, location, old_conn.carrier),
key=route.path,
)
return None
@component
def link(*children: VdomChild, to: str, **attributes: Any) -> VdomDict:
set_location = _use_route_state().set_location
attrs = {
**attributes,
"to": to,
"onClick": lambda event: set_location(Location(**event)),
}
return _link(attrs, *children)
def use_params() -> dict[str, Any]:
"""Get parameters from the currently matching route pattern"""
return _use_route_state().params
def use_query(
keep_blank_values: bool = False,
strict_parsing: bool = False,
errors: str = "replace",
max_num_fields: int | None = None,
separator: str = "&",
) -> dict[str, list[str]]:
"""See :func:`urllib.parse.parse_qs` for parameter info."""
return parse_qs(
use_location().search[1:],
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
errors=errors,
max_num_fields=max_num_fields,
separator=separator,
)
def _compile_routes(
routes: Sequence[R], compiler: RouteCompiler[R]
) -> list[tuple[Any, RoutePattern]]:
return [(r, compiler(r)) for r in _iter_routes(routes)]
def _iter_routes(routes: Sequence[R]) -> Iterator[R]:
for parent in routes:
for child in _iter_routes(parent.routes):
yield replace(child, path=parent.path + child.path)
yield parent
def _match_route(
compiled_routes: list[tuple[R, RoutePattern]], location: Location
) -> tuple[R, dict[str, Any]] | None:
for route, pattern in compiled_routes:
params = pattern.match(location.pathname)
if params is not None: # explicitely None check (could be empty dict)
return route, params
return None
_link = export(
module_from_file("idom-router", file=Path(__file__).parent / "bundle.js"),
"Link",
)
@dataclass
class _RouteState:
set_location: Callable[[Location], None]
params: dict[str, Any]
def _use_route_state() -> _RouteState:
route_state = use_context(_route_state_context)
assert route_state is not None
return route_state
_route_state_context: Context[_RouteState | None] = create_context(None)