Skip to content

Add docs #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 271 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,288 @@ cd reactpy-router
pip install -e . -r requirements.txt
```

# Running the Tests
# Usage

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

```bash
pip install -r requirements.txt
```python
from reactpy import component, html, run
from reactpy_router import route, simple

@component
def root():
return simple.router(
route("/", html.h1("Home Page 🏠")),
route("*", html.h1("Missing Link 🔗‍💥")),
)

run(root)
```

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

```bash
pytest tests
With this foundation you can start adding more routes:

```python
from reactpy import component, html, run
from reactpy_router import route, simple

@component
def root():
return simple.router(
route("/", html.h1("Home Page 🏠")),
route("/messages", html.h1("Messages 💬")),
route("*", html.h1("Missing Link 🔗‍💥")),
)

run(root)
```

With this change you can now also go to `/messages` to see "Messages 💬" displayed.

# Route Links

Instead of using the standard `<a>` element to create links to different parts of your
application, use `reactpy_router.link` instead. When users click links constructed using
`reactpy_router.link`, instead of letting the browser navigate to the associated route,
ReactPy will more quickly handle the transition by avoiding the cost of a full page
load.

```python
from reactpy import component, html, run
from reactpy_router import link, route, simple

@component
def root():
return simple.router(
route("/", home()),
route("/messages", html.h1("Messages 💬")),
route("*", html.h1("Missing Link 🔗‍💥")),
)

@component
def home():
return html.div(
html.h1("Home Page 🏠"),
link("Messages", to="/messages"),
)

run(root)
```

Now, when you go to the home page, you can click the link to go to `/messages`.

## Nested Routes

Routes can be nested in order to construct more complicated application structures:

```python
from reactpy import component, html, run
from reactpy_router import route, simple, link

message_data = [
{"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
{"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
{"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
{"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
{"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
{"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
{"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
{"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
]

@component
def root():
return simple.router(
route("/", home()),
route(
"/messages",
all_messages(),
# we'll improve upon these manually created routes in the next section...
route("/with/Alice", messages_with("Alice")),
route("/with/Alice-Bob", messages_with("Alice", "Bob")),
),
route("*", html.h1("Missing Link 🔗‍💥")),
)

@component
def home():
return html.div(
html.h1("Home Page 🏠"),
link("Messages", to="/messages"),
)

@component
def all_messages():
last_messages = {
", ".join(msg["with"]): msg
for msg in sorted(message_data, key=lambda m: m["id"])
}
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()
]
),
)

@component
def messages_with(*names):
names = set(names)
messages = [msg for msg in message_data if set(msg["with"]) == names]
return html.div(
html.h1(f"Messages with {', '.join(names)} 💬"),
html.ul(
[
html.li(
{"key": msg["id"]},
f"{msg['from'] or 'You'}: {msg['message']}",
)
for msg in messages
]
),
)
Copy link
Contributor

@Archmonger Archmonger May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think defining messages_with, all_messages, and message_data is overkill. Ends up distracting from the point (nested routes).

Since this is simply for demonstration purposes, I would either rewrite messages_with and all_messages to return one-liner dummy strings, or not use components at all and directly put raw strings into the routes (ex. html.p("EXAMPLE: All Messages")).


run(root)
```

## Route Parameters

In the example above we had to manually create a `messages_with(...)` component for each
conversation. This would be better accomplished by defining a single route that declares
a "route parameters" instead. With the `simple.router` route parameters are declared
using the following syntax:

```
/my/route/{param}
/my/route/{param:type}
```

In this case, `param` is the name of the route parameter and the optionally declared
`type` specifies what kind of parameter it is. The available parameter types and what
patterns they match are are:

- str (default) - `[^/]+`
- int - `\d+`
- float - `\d+(\.\d+)?`
- uuid - `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`
- path - `.+`

Any parameters that have matched in the currently displayed route can then be consumed
with the `use_params` hook which returns a dictionary mapping the parameter names to
their values. Note that parameters with a declared type will be converted to is in the
parameters dictionary. So for example `/my/route/{my_param:float}` would match
`/my/route/3.14` and have a parameter dictionary of `{"my_param": 3.14}`.

If we take this information and apply it to our growing example application we'd
substitute the manually constructed `/messages/with` routes with a single
`/messages/with/{names}` route:

```python
from reactpy import component, html, run
from reactpy_router import route, simple, link
from reactpy_router.core import use_params

message_data = [
{"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
{"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
{"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
{"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
{"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
{"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
{"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
{"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
]

@component
def root():
return simple.router(
route("/", home()),
route(
"/messages",
all_messages(),
route("/with/{names}", messages_with()), # note the path param
),
route("*", html.h1("Missing Link 🔗‍💥")),
)

@component
def home():
return html.div(
html.h1("Home Page 🏠"),
link("Messages", to="/messages"),
)

@component
def all_messages():
last_messages = {
", ".join(msg["with"]): msg
for msg in sorted(message_data, key=lambda m: m["id"])
}
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()
]
),
)

@component
def messages_with():
names = set(use_params()["names"].split("-")) # and here we use the path param
messages = [msg for msg in message_data if set(msg["with"]) == names]
return html.div(
html.h1(f"Messages with {', '.join(names)} 💬"),
html.ul(
[
html.li(
{"key": msg["id"]},
f"{msg['from'] or 'You'}: {msg['message']}",
)
for msg in messages
]
),
)

run(root)
Copy link
Contributor

@Archmonger Archmonger May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is WAY too long to explain the simple concept of encoding parameters. Needs to be rewritten with the most basic example possible.

Just need to get the idea across that parameters turn into args (maybe they can also become kwargs?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I think what I've written here is too long for a quick README that covers the features at a glance. However, this would still be useful to preserve, if explanations were expanded upon a bit, as a more in-depth tutorial. Is this something that you'd be comfortable with reworking, maybe with mkdocs?

Copy link
Contributor

@Archmonger Archmonger May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah when I get around to adding proper docs for this, I'll reference the commit log on this PR to fetch these examples. ReactPy docs and conreq dev comes first though.

```

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

```bash
pytest tests
nox -s test
```

You'll need to run in headless mode to execute the suite in continuous integration systems
like GitHub Actions.
You can run the tests with a headed browser.

```bash
nox -s test -- --headed
```

# Releasing This Package

Expand Down
2 changes: 1 addition & 1 deletion reactpy_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# the version is statically loaded by setup.py
__version__ = "0.0.1"
__version__ = "0.1.0"

from . import simple
from .core import create_router, link, route, router_component, use_params, use_query
Expand Down
3 changes: 3 additions & 0 deletions reactpy_router/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:


def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
if path == "*":
return re.compile(".*"), {}

pattern = "^"
last_match_end = 0
converters: ConverterMapping = {}
Expand Down
4 changes: 4 additions & 0 deletions tests/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ def test_parse_path_re_escape():
re.compile(r"^/a/(?P<b>\d+)/c\.d$"),
{"b": int},
)


def test_match_any_path():
assert parse_path("*") == (re.compile(".*"), {})