rio-labs / rio

WebApps in pure Python. No JavaScript, HTML and CSS needed
https://rio.dev
Apache License 2.0
1.4k stars 43 forks source link

[Feature Request] Dynamic routes #155

Open dunivlorraine opened 1 week ago

dunivlorraine commented 1 week ago

Description

Currently, there is no way to add a route that has a name that's not already pre-defined : /todos/[:id] where [:id] is a runtime-based value.

The url_segment parameter of a @rio.page annotation hasn't dynamic value slots for now.

Suggested Solution

Add slots that could be used in the url_segment parameter of a @rio.page annotation. Slots could be defined using a special suite of characters delimiting its value (e.g. like SvelteKit : [:my-path-name]). This slot may only allow a string value for now, as other kind of restrictions/typing for that slot value is quite an advanced feature.

The value of the slots should be accessible within the page and sub-pages (why not in the build context of the class ?).

Alternatives

Appart from generating pages at runtime or using URL query parameters (which both are not available features in rio-ui), I don't really see other alternatives.

Additional Context

No response

Related Issues/Pull Requests

No response

mad-moo commented 1 week ago

Yes, we absolutely want this. It's not high priority because there's an easy workaround: You can look up the current URL in the session, then dynamically display content based on that URL.

Here's a quick example to get you started:

class MyPage(rio.Component):
    def build(self) -> rio.Component:
        # Get a dynamic value from the current URL, such as the ID of a user
        #
        # The current URL is available in the session, as an instance of
        # `yarl.URL`. Yarls considers the first part to be `/`. The second part
        # will be `user`, so we're interested in the third part, i.e. index `2`.
        try:
            user_id = self.session.active_page_url.parts[2]

        # Fallback, in case the current URL is `/user` without an ID
        except IndexError:
            return rio.Text("No user selected")

        # Build the page, displaying information about the user ID. In a serious
        # app you'd probably want to fetch this information from a database.
        # (Always perform slow operations like database access OUTSIDE of
        # build!)
        return rio.Text(f"Hello, #{user_id}!")**
dunivlorraine commented 1 week ago

Great, that's good to know! I'll use this workaround in the meantime.

dunivlorraine commented 4 days ago

Hi! So I've tried this method and found out that that could not work: a real use-case page is when a route acts as a way to display both a list of records, and details of a record, and that we should navigate from the list to the details of a record using the session.navigate_to method.

Here's an example: /todos -> list all todos /todos/id -> display the todo details with that ID

In the todos_page.py build:

try:
    todo_id = self.session.active_page_url.parts[2]
# Fallback, in case the current URL is without an ID
except IndexError:
    return self.display_list()
return self.display_details(todo_id)

self.display_list() returns a list of cards representing todo records. Each card has the following props:

on_press=lambda: self.session.navigate_to(f"/todos/{self.id}")

If we click on a card, we won't switch to the details view, as the page build function won't be called again when we use the session.navigate_to function.

dunivlorraine commented 4 days ago

I could use another page to do it (/todos/details/id), but it's not really a best practices.

dunivlorraine commented 4 days ago

Also, we can't develop routes that are based on dynamic ones outside the page that has the slot. Here's an example: /todos/:id/create Here, it's not possible to handle the "create" logic outside the todos_page.py, which means that we should handle every possible routes after the dynamic slot in the same page.

mad-moo commented 4 days ago

It's a bit unusual to use the same page to do two things. Usually you'd have something like/items and /item/<id>. If you want to use the same page for both, you can always force a refresh using await self.force_refresh(). Alaternatively, use the @rio.event.on_page_change to get notified when you need to display something else

dunivlorraine commented 1 day ago

IMHO, plural names is the most common way of using dynamic routes, there are many websites who chose to do that (here, Github for issues) for several reasons:

Although, singular naming is also used, it mainly depends of the context, and I understand that plural vs singular naming is a developer choice rather a rule. And this implies that all the logic for these sub-pages must be handled inside the same page.

I understand that rio-ui is in development and is young, so I'll stick to that for now, I just think that the workaround is not really a full one, as it only covers parts of what dynamic routing provide :)