Skip to content

Commit 8e9b6a8

Browse files
committed
use attributes dict for all link parameters
1 parent 25e440f commit 8e9b6a8

File tree

7 files changed

+82
-73
lines changed

7 files changed

+82
-73
lines changed

docs/examples/python/nested-routes.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,26 @@ def root():
3535
def home():
3636
return html.div(
3737
html.h1("Home Page 🏠"),
38-
link("Messages", to="/messages"),
38+
link({"to": "/messages"}, "Messages"),
3939
)
4040

4141

4242
@component
4343
def all_messages():
4444
last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])}
45+
46+
messages = []
47+
for msg in last_messages.values():
48+
_link = link(
49+
{"to": f"/messages/with/{'-'.join(msg['with'])}"},
50+
f"Conversation with: {', '.join(msg['with'])}",
51+
)
52+
msg_from = f"{'' if msg['from'] is None else '🔴'} {msg['message']}"
53+
messages.append(html.li({"key": msg["id"]}, html.p(_link), msg_from))
54+
4555
return html.div(
4656
html.h1("All Messages 💬"),
47-
html.ul(
48-
[
49-
html.li(
50-
{"key": msg["id"]},
51-
html.p(
52-
link(
53-
f"Conversation with: {', '.join(msg['with'])}",
54-
to=f"/messages/with/{'-'.join(msg['with'])}",
55-
),
56-
),
57-
f"{'' if msg['from'] is None else '🔴'} {msg['message']}",
58-
)
59-
for msg in last_messages.values()
60-
]
61-
),
57+
html.ul(messages),
6258
)
6359

6460

docs/examples/python/route-links.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from reactpy import component, html, run
2+
23
from reactpy_router import browser_router, link, route
34

45

@@ -15,7 +16,7 @@ def root():
1516
def home():
1617
return html.div(
1718
html.h1("Home Page 🏠"),
18-
link("Messages", to="/messages"),
19+
link({"to": "/messages"}, "Messages"),
1920
)
2021

2122

docs/examples/python/route-parameters.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,25 @@ def root():
3333
def home():
3434
return html.div(
3535
html.h1("Home Page 🏠"),
36-
link("Messages", to="/messages"),
36+
link({"to": "/messages"}, "Messages"),
3737
)
3838

3939

4040
@component
4141
def all_messages():
4242
last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])}
43+
messages = []
44+
for msg in last_messages.values():
45+
msg_hyperlink = link(
46+
{"to": f"/messages/with/{'-'.join(msg['with'])}"},
47+
f"Conversation with: {', '.join(msg['with'])}",
48+
)
49+
msg_from = f"{'' if msg['from'] is None else '🔴'} {msg['message']}"
50+
messages.append(html.li({"key": msg["id"]}, html.p(msg_hyperlink), msg_from))
51+
4352
return html.div(
4453
html.h1("All Messages 💬"),
45-
html.ul(
46-
[
47-
html.li(
48-
{"key": msg["id"]},
49-
html.p(
50-
link(
51-
f"Conversation with: {', '.join(msg['with'])}",
52-
to=f"/messages/with/{'-'.join(msg['with'])}",
53-
),
54-
),
55-
f"{'' if msg['from'] is None else '🔴'} {msg['message']}",
56-
)
57-
for msg in last_messages.values()
58-
]
59-
),
54+
html.ul(messages),
6055
)
6156

6257

docs/examples/python/use-params.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def root():
1616
"/",
1717
html.div(
1818
html.h1("Home Page 🏠"),
19-
link("User 123", to="/user/123"),
19+
link({"to": "/user/123"}, "User 123"),
2020
),
2121
),
2222
route("/user/{id:int}", user()),

docs/examples/python/use-search-params.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def root():
1616
"/",
1717
html.div(
1818
html.h1("Home Page 🏠"),
19-
link("Search", to="/search?query=reactpy"),
19+
link({"to": "/search?query=reactpy"}, "Search"),
2020
),
2121
),
2222
route("/search", search()),

src/reactpy_router/components.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from reactpy import component, html
88
from reactpy.backend.types import Location
9+
from reactpy.core.component import Component
910
from reactpy.core.types import VdomDict
1011
from reactpy.web.module import export, module_from_file
1112

@@ -16,32 +17,37 @@
1617
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
1718
("History"),
1819
)
20+
"""Client-side portion of history handling"""
21+
1922
Link = export(
2023
module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"),
2124
("Link"),
2225
)
26+
"""Client-side portion of link handling"""
27+
28+
29+
def link(attributes: dict[str, Any], *children: Any) -> Component:
30+
"""Create a link with the given attributes and children."""
31+
return _link(attributes, *children)
2332

2433

2534
@component
26-
def link(*attributes_and_children: Any, to: str | None = None, **kwargs: Any) -> VdomDict:
35+
def _link(attributes: dict[str, Any], *children: Any) -> VdomDict:
2736
"""A component that renders a link to the given path."""
28-
if to is None:
29-
raise ValueError("The `to` attribute is required for the `Link` component.")
30-
37+
attributes = attributes.copy()
3138
uuid_string = f"link-{uuid4().hex}"
3239
class_name = f"{uuid_string}"
3340
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:]
4041
if "className" in attributes:
4142
class_name = " ".join([attributes.pop("className"), class_name])
4243
if "class_name" in attributes: # pragma: no cover
4344
# TODO: This can be removed when ReactPy stops supporting underscores in attribute names
4445
class_name = " ".join([attributes.pop("class_name"), class_name])
46+
if "href" in attributes and "to" not in attributes:
47+
attributes["to"] = attributes.pop("href")
48+
if "to" not in attributes:
49+
raise ValueError("The `to` attribute is required for the `Link` component.")
50+
to = attributes.pop("to")
4551

4652
attrs = {
4753
**attributes,
@@ -52,7 +58,7 @@ def link(*attributes_and_children: Any, to: str | None = None, **kwargs: Any) ->
5258
def on_click(_event: dict[str, Any]) -> None:
5359
set_location(Location(**_event))
5460

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

5763

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

tests/test_core.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,18 @@ async def test_navigate_with_link(display: DisplayFixture):
8585
def sample():
8686
render_count.current += 1
8787
return browser_router(
88-
route("/", link("Root", to="/a", id="root")),
89-
route("/a", link("A", to="/b", id="a")),
90-
route("/b", link("B", to="/c", id="b")),
91-
route("/c", link("C", to="/default", id="c")),
88+
route("/", link({"to": "/a", "id": "root"}, "Root")),
89+
route("/a", link({"to": "/b", "id": "a"}, "A")),
90+
route("/b", link({"to": "/c", "id": "b"}, "B")),
91+
route("/c", link({"to": "/default", "id": "c"}, "C")),
9292
route("{default:any}", html.h1({"id": "default"}, "Default")),
9393
)
9494

9595
await display.show(sample)
9696

9797
for link_selector in ["#root", "#a", "#b", "#c"]:
98-
lnk = await display.page.wait_for_selector(link_selector)
99-
await lnk.click(delay=CLICK_DELAY)
98+
_link = await display.page.wait_for_selector(link_selector)
99+
await _link.click(delay=CLICK_DELAY)
100100

101101
await display.page.wait_for_selector("#default")
102102

@@ -164,18 +164,18 @@ async def test_browser_popstate(display: DisplayFixture):
164164
@component
165165
def sample():
166166
return browser_router(
167-
route("/", link("Root", to="/a", id="root")),
168-
route("/a", link("A", to="/b", id="a")),
169-
route("/b", link("B", to="/c", id="b")),
170-
route("/c", link("C", to="/default", id="c")),
167+
route("/", link({"to": "/a", "id": "root"}, "Root")),
168+
route("/a", link({"to": "/b", "id": "a"}, "A")),
169+
route("/b", link({"to": "/c", "id": "b"}, "B")),
170+
route("/c", link({"to": "/default", "id": "c"}, "C")),
171171
route("{default:any}", html.h1({"id": "default"}, "Default")),
172172
)
173173

174174
await display.show(sample)
175175

176176
for link_selector in ["#root", "#a", "#b", "#c"]:
177-
lnk = await display.page.wait_for_selector(link_selector)
178-
await lnk.click(delay=CLICK_DELAY)
177+
_link = await display.page.wait_for_selector(link_selector)
178+
await _link.click(delay=CLICK_DELAY)
179179

180180
await display.page.wait_for_selector("#default")
181181

@@ -196,21 +196,21 @@ async def test_relative_links(display: DisplayFixture):
196196
@component
197197
def sample():
198198
return browser_router(
199-
route("/", link("Root", to="/a", id="root")),
200-
route("/a", link("A", to="/a/a/../b", id="a")),
201-
route("/a/b", link("B", to="../a/b/c", id="b")),
202-
route("/a/b/c", link("C", to="../d", id="c")),
203-
route("/a/d", link("D", to="e", id="d")),
204-
route("/a/e", link("E", to="/a/./f", id="e")),
205-
route("/a/f", link("F", to="../default", id="f")),
199+
route("/", link({"to": "a", "id": "root"}, "Root")),
200+
route("/a", link({"to": "a/a/../b", "id": "a"}, "A")),
201+
route("/a/b", link({"to": "../a/b/c", "id": "b"}, "B")),
202+
route("/a/b/c", link({"to": "../d", "id": "c"}, "C")),
203+
route("/a/d", link({"to": "e", "id": "d"}, "D")),
204+
route("/a/e", link({"to": "/a/./f", "id": "e"}, "E")),
205+
route("/a/f", link({"to": "../default", "id": "f"}, "F")),
206206
route("{default:any}", html.h1({"id": "default"}, "Default")),
207207
)
208208

209209
await display.show(sample)
210210

211211
for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e", "#f"]:
212-
lnk = await display.page.wait_for_selector(link_selector)
213-
await lnk.click(delay=CLICK_DELAY)
212+
_link = await display.page.wait_for_selector(link_selector)
213+
await _link.click(delay=CLICK_DELAY)
214214

215215
await display.page.wait_for_selector("#default")
216216

@@ -246,23 +246,34 @@ def check_search_params():
246246
@component
247247
def sample():
248248
return browser_router(
249-
route("/", link("Root", to="/a?a=1&b=2", id="root")),
249+
route("/", link({"to": "/a?a=1&b=2", "id": "root"}, "Root")),
250250
route("/a", check_search_params()),
251251
)
252252

253253
await display.show(sample)
254254
await display.page.wait_for_selector("#root")
255-
lnk = await display.page.wait_for_selector("#root")
256-
await lnk.click(delay=CLICK_DELAY)
255+
_link = await display.page.wait_for_selector("#root")
256+
await _link.click(delay=CLICK_DELAY)
257257
await display.page.wait_for_selector("#success")
258258

259259

260260
async def test_link_class_name(display: DisplayFixture):
261261
@component
262262
def sample():
263-
return browser_router(route("/", link("Root", to="/a", id="root", className="class1")))
263+
return browser_router(route("/", link({"to": "/a", "id": "root", "className": "class1"}, "Root")))
264264

265265
await display.show(sample)
266266

267-
lnk = await display.page.wait_for_selector("#root")
268-
assert "class1" in await lnk.get_attribute("class")
267+
_link = await display.page.wait_for_selector("#root")
268+
assert "class1" in await _link.get_attribute("class")
269+
270+
271+
async def test_link_href(display: DisplayFixture):
272+
@component
273+
def sample():
274+
return browser_router(route("/", link({"href": "/a", "id": "root"}, "Root")))
275+
276+
await display.show(sample)
277+
278+
_link = await display.page.wait_for_selector("#root")
279+
assert "/a" in await _link.get_attribute("href")

0 commit comments

Comments
 (0)