From 8e9b6a8074d574bd652205d3a516bbde012ae43d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 12 Oct 2024 03:02:26 -0700 Subject: [PATCH] use attributes dict for all link parameters --- docs/examples/python/nested-routes.py | 28 +++++----- docs/examples/python/route-links.py | 3 +- docs/examples/python/route-parameters.py | 27 ++++------ docs/examples/python/use-params.py | 2 +- docs/examples/python/use-search-params.py | 2 +- src/reactpy_router/components.py | 28 ++++++---- tests/test_core.py | 65 +++++++++++++---------- 7 files changed, 82 insertions(+), 73 deletions(-) diff --git a/docs/examples/python/nested-routes.py b/docs/examples/python/nested-routes.py index 332adbb..01ffb18 100644 --- a/docs/examples/python/nested-routes.py +++ b/docs/examples/python/nested-routes.py @@ -35,30 +35,26 @@ def root(): def home(): return html.div( html.h1("Home Page 🏠"), - link("Messages", to="/messages"), + link({"to": "/messages"}, "Messages"), ) @component def all_messages(): last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])} + + messages = [] + for msg in last_messages.values(): + _link = link( + {"to": f"/messages/with/{'-'.join(msg['with'])}"}, + f"Conversation with: {', '.join(msg['with'])}", + ) + msg_from = f"{'' if msg['from'] is None else '🔴'} {msg['message']}" + messages.append(html.li({"key": msg["id"]}, html.p(_link), msg_from)) + return html.div( html.h1("All Messages 💬"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else '🔴'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), + html.ul(messages), ) diff --git a/docs/examples/python/route-links.py b/docs/examples/python/route-links.py index 46c98d3..baf428c 100644 --- a/docs/examples/python/route-links.py +++ b/docs/examples/python/route-links.py @@ -1,4 +1,5 @@ from reactpy import component, html, run + from reactpy_router import browser_router, link, route @@ -15,7 +16,7 @@ def root(): def home(): return html.div( html.h1("Home Page 🏠"), - link("Messages", to="/messages"), + link({"to": "/messages"}, "Messages"), ) diff --git a/docs/examples/python/route-parameters.py b/docs/examples/python/route-parameters.py index a2e6707..a794742 100644 --- a/docs/examples/python/route-parameters.py +++ b/docs/examples/python/route-parameters.py @@ -33,30 +33,25 @@ def root(): def home(): return html.div( html.h1("Home Page 🏠"), - link("Messages", to="/messages"), + link({"to": "/messages"}, "Messages"), ) @component def all_messages(): last_messages = {", ".join(msg["with"]): msg for msg in sorted(message_data, key=lambda m: m["id"])} + messages = [] + for msg in last_messages.values(): + msg_hyperlink = link( + {"to": f"/messages/with/{'-'.join(msg['with'])}"}, + f"Conversation with: {', '.join(msg['with'])}", + ) + msg_from = f"{'' if msg['from'] is None else '🔴'} {msg['message']}" + messages.append(html.li({"key": msg["id"]}, html.p(msg_hyperlink), msg_from)) + return html.div( html.h1("All Messages 💬"), - html.ul( - [ - html.li( - {"key": msg["id"]}, - html.p( - link( - f"Conversation with: {', '.join(msg['with'])}", - to=f"/messages/with/{'-'.join(msg['with'])}", - ), - ), - f"{'' if msg['from'] is None else '🔴'} {msg['message']}", - ) - for msg in last_messages.values() - ] - ), + html.ul(messages), ) diff --git a/docs/examples/python/use-params.py b/docs/examples/python/use-params.py index 76a94df..93a4f07 100644 --- a/docs/examples/python/use-params.py +++ b/docs/examples/python/use-params.py @@ -16,7 +16,7 @@ def root(): "/", html.div( html.h1("Home Page 🏠"), - link("User 123", to="/user/123"), + link({"to": "/user/123"}, "User 123"), ), ), route("/user/{id:int}", user()), diff --git a/docs/examples/python/use-search-params.py b/docs/examples/python/use-search-params.py index 6d3cac0..faeba5e 100644 --- a/docs/examples/python/use-search-params.py +++ b/docs/examples/python/use-search-params.py @@ -16,7 +16,7 @@ def root(): "/", html.div( html.h1("Home Page 🏠"), - link("Search", to="/search?query=reactpy"), + link({"to": "/search?query=reactpy"}, "Search"), ), ), route("/search", search()), diff --git a/src/reactpy_router/components.py b/src/reactpy_router/components.py index 37214d8..9e4701c 100644 --- a/src/reactpy_router/components.py +++ b/src/reactpy_router/components.py @@ -6,6 +6,7 @@ from reactpy import component, html from reactpy.backend.types import Location +from reactpy.core.component import Component from reactpy.core.types import VdomDict from reactpy.web.module import export, module_from_file @@ -16,32 +17,37 @@ module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), ("History"), ) +"""Client-side portion of history handling""" + Link = export( module_from_file("reactpy-router", file=Path(__file__).parent / "static" / "bundle.js"), ("Link"), ) +"""Client-side portion of link handling""" + + +def link(attributes: dict[str, Any], *children: Any) -> Component: + """Create a link with the given attributes and children.""" + return _link(attributes, *children) @component -def link(*attributes_and_children: Any, to: str | None = None, **kwargs: Any) -> VdomDict: +def _link(attributes: dict[str, Any], *children: Any) -> VdomDict: """A component that renders a link to the given path.""" - if to is None: - raise ValueError("The `to` attribute is required for the `Link` component.") - + attributes = attributes.copy() 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]) 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]) + if "href" in attributes and "to" not in attributes: + attributes["to"] = attributes.pop("href") + if "to" not in attributes: + raise ValueError("The `to` attribute is required for the `Link` component.") + to = attributes.pop("to") attrs = { **attributes, @@ -52,7 +58,7 @@ def link(*attributes_and_children: Any, to: str | None = None, **kwargs: Any) -> 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})) + return html._(html.a(attrs, *children), Link({"onClick": on_click, "linkClass": uuid_string})) def route(path: str, element: Any | None, *routes: Route) -> Route: diff --git a/tests/test_core.py b/tests/test_core.py index 9bdfde8..959643d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -85,18 +85,18 @@ async def test_navigate_with_link(display: DisplayFixture): def sample(): render_count.current += 1 return browser_router( - route("/", link("Root", to="/a", id="root")), - route("/a", link("A", to="/b", id="a")), - route("/b", link("B", to="/c", id="b")), - route("/c", link("C", to="/default", id="c")), + route("/", link({"to": "/a", "id": "root"}, "Root")), + route("/a", link({"to": "/b", "id": "a"}, "A")), + route("/b", link({"to": "/c", "id": "b"}, "B")), + route("/c", link({"to": "/default", "id": "c"}, "C")), route("{default:any}", html.h1({"id": "default"}, "Default")), ) await display.show(sample) for link_selector in ["#root", "#a", "#b", "#c"]: - lnk = await display.page.wait_for_selector(link_selector) - await lnk.click(delay=CLICK_DELAY) + _link = await display.page.wait_for_selector(link_selector) + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") @@ -164,18 +164,18 @@ async def test_browser_popstate(display: DisplayFixture): @component def sample(): return browser_router( - route("/", link("Root", to="/a", id="root")), - route("/a", link("A", to="/b", id="a")), - route("/b", link("B", to="/c", id="b")), - route("/c", link("C", to="/default", id="c")), + route("/", link({"to": "/a", "id": "root"}, "Root")), + route("/a", link({"to": "/b", "id": "a"}, "A")), + route("/b", link({"to": "/c", "id": "b"}, "B")), + route("/c", link({"to": "/default", "id": "c"}, "C")), route("{default:any}", html.h1({"id": "default"}, "Default")), ) await display.show(sample) for link_selector in ["#root", "#a", "#b", "#c"]: - lnk = await display.page.wait_for_selector(link_selector) - await lnk.click(delay=CLICK_DELAY) + _link = await display.page.wait_for_selector(link_selector) + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") @@ -196,21 +196,21 @@ async def test_relative_links(display: DisplayFixture): @component def sample(): return browser_router( - route("/", link("Root", to="/a", id="root")), - route("/a", link("A", to="/a/a/../b", id="a")), - route("/a/b", link("B", to="../a/b/c", id="b")), - route("/a/b/c", link("C", to="../d", id="c")), - route("/a/d", link("D", to="e", id="d")), - route("/a/e", link("E", to="/a/./f", id="e")), - route("/a/f", link("F", to="../default", id="f")), + route("/", link({"to": "a", "id": "root"}, "Root")), + route("/a", link({"to": "a/a/../b", "id": "a"}, "A")), + route("/a/b", link({"to": "../a/b/c", "id": "b"}, "B")), + route("/a/b/c", link({"to": "../d", "id": "c"}, "C")), + route("/a/d", link({"to": "e", "id": "d"}, "D")), + route("/a/e", link({"to": "/a/./f", "id": "e"}, "E")), + route("/a/f", link({"to": "../default", "id": "f"}, "F")), route("{default:any}", html.h1({"id": "default"}, "Default")), ) await display.show(sample) for link_selector in ["#root", "#a", "#b", "#c", "#d", "#e", "#f"]: - lnk = await display.page.wait_for_selector(link_selector) - await lnk.click(delay=CLICK_DELAY) + _link = await display.page.wait_for_selector(link_selector) + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#default") @@ -246,23 +246,34 @@ def check_search_params(): @component def sample(): return browser_router( - route("/", link("Root", to="/a?a=1&b=2", id="root")), + route("/", link({"to": "/a?a=1&b=2", "id": "root"}, "Root")), route("/a", check_search_params()), ) await display.show(sample) await display.page.wait_for_selector("#root") - lnk = await display.page.wait_for_selector("#root") - await lnk.click(delay=CLICK_DELAY) + _link = await display.page.wait_for_selector("#root") + await _link.click(delay=CLICK_DELAY) await display.page.wait_for_selector("#success") async def test_link_class_name(display: DisplayFixture): @component def sample(): - return browser_router(route("/", link("Root", to="/a", id="root", className="class1"))) + return browser_router(route("/", link({"to": "/a", "id": "root", "className": "class1"}, "Root"))) await display.show(sample) - lnk = await display.page.wait_for_selector("#root") - assert "class1" in await lnk.get_attribute("class") + _link = await display.page.wait_for_selector("#root") + assert "class1" in await _link.get_attribute("class") + + +async def test_link_href(display: DisplayFixture): + @component + def sample(): + return browser_router(route("/", link({"href": "/a", "id": "root"}, "Root"))) + + await display.show(sample) + + _link = await display.page.wait_for_selector("#root") + assert "/a" in await _link.get_attribute("href")