Skip to content

Commit 78dc291

Browse files
committed
initial docs + * match
1 parent a6f7d23 commit 78dc291

File tree

4 files changed

+279
-14
lines changed

4 files changed

+279
-14
lines changed

README.md

+271-13
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,288 @@ cd reactpy-router
1818
pip install -e . -r requirements.txt
1919
```
2020

21-
# Running the Tests
21+
# Usage
2222

23-
To run the tests you'll need to install [Chrome](https://www.google.com/chrome/). Then you
24-
can download the [ChromeDriver](https://chromedriver.chromium.org/downloads) and add it to
25-
your `PATH`. Once that's done, simply `pip` install the requirements:
23+
Assuming you are familiar with the basics of [ReactPy](https://reactpy.dev), you can
24+
begin by using the simple built-in router implementation supplied by `reactpy-router`.
2625

27-
```bash
28-
pip install -r requirements.txt
26+
```python
27+
from reactpy import component, html, run
28+
from reactpy_router import route, simple
29+
30+
@component
31+
def root():
32+
return simple.router(
33+
route("/", html.h1("Home Page 🏠")),
34+
route("*", html.h1("Missing Link 🔗‍💥")),
35+
)
36+
37+
run(root)
2938
```
3039

31-
And run the tests with `pytest`:
40+
When navigating to http://127.0.0.1:8000 you should see "Home Page 🏠". However, if you
41+
go to any other route (e.g. http://127.0.0.1:8000/missing) you will instead see the
42+
"Missing Link 🔗‍💥" page.
3243

33-
```bash
34-
pytest tests
44+
With this foundation you can start adding more routes:
45+
46+
```python
47+
from reactpy import component, html, run
48+
from reactpy_router import route, simple
49+
50+
@component
51+
def root():
52+
return simple.router(
53+
route("/", html.h1("Home Page 🏠")),
54+
route("/messages", html.h1("Messages 💬")),
55+
route("*", html.h1("Missing Link 🔗‍💥")),
56+
)
57+
58+
run(root)
59+
```
60+
61+
With this change you can now also go to `/messages` to see "Messages 💬" displayed.
62+
63+
# Route Links
64+
65+
Instead of using the standard `<a>` element to create links to different parts of your
66+
application, use `reactpy_router.link` instead. When users click links constructed using
67+
`reactpy_router.link`, instead of letting the browser navigate to the associated route,
68+
ReactPy will more quickly handle the transition by avoiding the cost of a full page
69+
load.
70+
71+
```python
72+
from reactpy import component, html, run
73+
from reactpy_router import link, route, simple
74+
75+
@component
76+
def root():
77+
return simple.router(
78+
route("/", home()),
79+
route("/messages", html.h1("Messages 💬")),
80+
route("*", html.h1("Missing Link 🔗‍💥")),
81+
)
82+
83+
@component
84+
def home():
85+
return html.div(
86+
html.h1("Home Page 🏠"),
87+
link("Messages", to="/messages"),
88+
)
89+
90+
run(root)
91+
```
92+
93+
Now, when you go to the home page, you can click the link to go to `/messages`.
94+
95+
## Nested Routes
96+
97+
Routes can be nested in order to construct more complicated application structures:
98+
99+
```python
100+
from reactpy import component, html, run
101+
from reactpy_router import route, simple, link
102+
103+
message_data = [
104+
{"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
105+
{"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
106+
{"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
107+
{"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
108+
{"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
109+
{"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
110+
{"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
111+
{"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
112+
]
113+
114+
@component
115+
def root():
116+
return simple.router(
117+
route("/", home()),
118+
route(
119+
"/messages",
120+
all_messages(),
121+
# we'll improve upon these manually created routes in the next section...
122+
route("/with/Alice", messages_with("Alice")),
123+
route("/with/Alice-Bob", messages_with("Alice", "Bob")),
124+
),
125+
route("*", html.h1("Missing Link 🔗‍💥")),
126+
)
127+
128+
@component
129+
def home():
130+
return html.div(
131+
html.h1("Home Page 🏠"),
132+
link("Messages", to="/messages"),
133+
)
134+
135+
@component
136+
def all_messages():
137+
last_messages = {
138+
", ".join(msg["with"]): msg
139+
for msg in sorted(message_data, key=lambda m: m["id"])
140+
}
141+
return html.div(
142+
html.h1("All Messages 💬"),
143+
html.ul(
144+
[
145+
html.li(
146+
{"key": msg["id"]},
147+
html.p(
148+
link(
149+
f"Conversation with: {', '.join(msg['with'])}",
150+
to=f"/messages/with/{'-'.join(msg['with'])}",
151+
),
152+
),
153+
f"{'' if msg['from'] is None else '🔴'} {msg['message']}",
154+
)
155+
for msg in last_messages.values()
156+
]
157+
),
158+
)
159+
160+
@component
161+
def messages_with(*names):
162+
names = set(names)
163+
messages = [msg for msg in message_data if set(msg["with"]) == names]
164+
return html.div(
165+
html.h1(f"Messages with {', '.join(names)} 💬"),
166+
html.ul(
167+
[
168+
html.li(
169+
{"key": msg["id"]},
170+
f"{msg['from'] or 'You'}: {msg['message']}",
171+
)
172+
for msg in messages
173+
]
174+
),
175+
)
176+
177+
run(root)
178+
```
179+
180+
## Route Parameters
181+
182+
In the example above we had to manually create a `messages_with(...)` component for each
183+
conversation. This would be better accomplished by defining a single route that declares
184+
a "route parameters" instead. With the `simple.router` route parameters are declared
185+
using the following syntax:
186+
187+
```
188+
/my/route/{param}
189+
/my/route/{param:type}
190+
```
191+
192+
In this case, `param` is the name of the route parameter and the optionally declared
193+
`type` specifies what kind of parameter it is. The available parameter types and what
194+
patterns they match are are:
195+
196+
- str (default) - `[^/]+`
197+
- int - `\d+`
198+
- float - `\d+(\.\d+)?`
199+
- uuid - `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`
200+
- path - `.+`
201+
202+
Any parameters that have matched in the currently displayed route can then be consumed
203+
with the `use_params` hook which returns a dictionary mapping the parameter names to
204+
their values. Note that parameters with a declared type will be converted to is in the
205+
parameters dictionary. So for example `/my/route/{my_param:float}` would match
206+
`/my/route/3.14` and have a parameter dictionary of `{"my_param": 3.14}`.
207+
208+
If we take this information and apply it to our growing example application we'd
209+
substitute the manually constructed `/messages/with` routes with a single
210+
`/messages/with/{names}` route:
211+
212+
```python
213+
from reactpy import component, html, run
214+
from reactpy_router import route, simple, link
215+
from reactpy_router.core import use_params
216+
217+
message_data = [
218+
{"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
219+
{"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
220+
{"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
221+
{"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
222+
{"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
223+
{"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
224+
{"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
225+
{"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
226+
]
227+
228+
@component
229+
def root():
230+
return simple.router(
231+
route("/", home()),
232+
route(
233+
"/messages",
234+
all_messages(),
235+
route("/with/{names}", messages_with()), # note the path param
236+
),
237+
route("*", html.h1("Missing Link 🔗‍💥")),
238+
)
239+
240+
@component
241+
def home():
242+
return html.div(
243+
html.h1("Home Page 🏠"),
244+
link("Messages", to="/messages"),
245+
)
246+
247+
@component
248+
def all_messages():
249+
last_messages = {
250+
", ".join(msg["with"]): msg
251+
for msg in sorted(message_data, key=lambda m: m["id"])
252+
}
253+
return html.div(
254+
html.h1("All Messages 💬"),
255+
html.ul(
256+
[
257+
html.li(
258+
{"key": msg["id"]},
259+
html.p(
260+
link(
261+
f"Conversation with: {', '.join(msg['with'])}",
262+
to=f"/messages/with/{'-'.join(msg['with'])}",
263+
),
264+
),
265+
f"{'' if msg['from'] is None else '🔴'} {msg['message']}",
266+
)
267+
for msg in last_messages.values()
268+
]
269+
),
270+
)
271+
272+
@component
273+
def messages_with():
274+
names = set(use_params()["names"].split("-")) # and here we use the path param
275+
messages = [msg for msg in message_data if set(msg["with"]) == names]
276+
return html.div(
277+
html.h1(f"Messages with {', '.join(names)} 💬"),
278+
html.ul(
279+
[
280+
html.li(
281+
{"key": msg["id"]},
282+
f"{msg['from'] or 'You'}: {msg['message']}",
283+
)
284+
for msg in messages
285+
]
286+
),
287+
)
288+
289+
run(root)
35290
```
36291

37-
You can run the tests in headless mode (i.e. without opening the browser):
292+
# Running the Tests
38293

39294
```bash
40-
pytest tests
295+
nox -s test
41296
```
42297

43-
You'll need to run in headless mode to execute the suite in continuous integration systems
44-
like GitHub Actions.
298+
You can run the tests with a headed browser.
299+
300+
```bash
301+
nox -s test -- --headed
302+
```
45303

46304
# Releasing This Package
47305

reactpy_router/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# the version is statically loaded by setup.py
2-
__version__ = "0.0.1"
2+
__version__ = "0.1.0"
33

44
from . import simple
55
from .core import create_router, link, route, router_component, use_params, use_query

reactpy_router/simple.py

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
3434

3535

3636
def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
37+
if path == "*":
38+
return re.compile(".*"), {}
39+
3740
pattern = "^"
3841
last_match_end = 0
3942
converters: ConverterMapping = {}

tests/test_simple.py

+4
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ def test_parse_path_re_escape():
4848
re.compile(r"^/a/(?P<b>\d+)/c\.d$"),
4949
{"b": int},
5050
)
51+
52+
53+
def test_match_any_path():
54+
assert parse_path("*") == (re.compile(".*"), {})

0 commit comments

Comments
 (0)