Skip to content
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

Referable routes #3445

Open
lpotthast opened this issue Jan 4, 2025 · 0 comments
Open

Referable routes #3445

lpotthast opened this issue Jan 4, 2025 · 0 comments

Comments

@lpotthast
Copy link
Contributor

Hey @gbj,
created a small Leptos library today, making it possible to define routes through a series of module declarations and a new attribute macro.

Lib lives here: https://github.com/lpotthast/leptos-routes/tree/main
Implementation is rather "quick-and-dirty" and most likely misses a lot of cases.

I would be interested in your thoughts / opinions on it:

  • Is it even relevant in your eyes to have something like this?
  • Any ideas for a better structure, meaning something other than mods to declare the routes? I couldn't find anything that is "mostly Rust" and doesn't require a user to learn a new syntax/macro-DSL
  • Do you see any way that this could be made unnecessary by other changes in leptos-router? Macros sadly always add a lot of complexity...
  • Do you think I should publish this? Wouldn't want to do it (under this name at least) without any feedback and approval.

A few ideas:

  • If it would be true that there is only ever one "right" or even mostly-right way of writing "the router" (in terms of leptos_router components)
<Router>
  <Routes>
    ....
  </Routes>
</Router>

I could also add a routes::router() function to the generation that simply builds that router view code for the entire app. This would make it even easier, and potentially less error-prone, to get your apps routing up to speed... It would require defining the view for each route inside the attribute macro, as in

#[route("/users", view=UsersPage)]
pub mod users {}

Maybe you could then write an app like this:

#[routes]
pub mod routes {
    #[route("/", view=HomePage)]
    pub mod root {}
}

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();

    view! {
        <Meta name="charset" content="UTF-8"/>-template-ssr.css"/>
        <Title text="Leptonic CSR template"/>

        <main>
            { routes::router() }
        </main>
    }
}

Just pasting the current README in here (this may be outdated)


leptos-routes

Declaratively define the routes for your Leptos project.

Example

use leptos_routes::routes;

#[routes]
pub mod routes {
    #[route("/")]
    pub mod root {}

    #[route("/users")]
    pub mod users {
      
        #[route("/:id")]
        pub mod user {
          
            #[route("/details")]
            pub mod details {}
        }
    }
}

What does it do?

The routes proc-macro parses the module hierarchy and generates a struct for each individual route in your
application. You can access these routes through your routes module and any of its submodules.

The example above will create the following structs (the module names determine the struct names, the module hierarchy
will be kept.):

let _ = routes::Root;
let _ = routes::Users;
let _ = routes::users::User;
let _ = routes::users::user::Details;

Each of these structs implements the following functions:

  • path() -> Segments, where Segments is a dynamically sized tuple based on the amount of segments present in the
    path passed to route. This only returns the segments declared on the currently evaluated mod itself and does not
    also include all of its prent segments.

    This makes path usable in <Route> declarations, where you otherwise would have directly used the path! macro
    from leptos_router, or anywhere else where some kind of Segments are required.

    use assertr::prelude::*;
    assert_that(routes::users::User.path()).is_equal_to((ParamSegment("id"),));
  • materialize(...) -> String materializes a usable URL path from the full list of path-segments, including all parent
    segments.

    Use it to create links to parts of your application. As IntoHref is implemented for String, the return value can
    be used anywhere an IntoHref is required, for example Leptos's <A> component!

    This function is automatically generated to take all necessary user-inputs in order to replace
    all dynamic path segments with concrete values, meaning that materialize might take 0-n inputs when the full path
    has n segments.

    use assertr::prelude::*;
    assert_that(routes::users::user::Details.materialize("42")).is_equal_to("/users/42/details");

Motivation

Having this router declaration

<Router>
  <Routes>
    <Route path=path!("/") view=Home/>
    <ParentRoute path=path!("/users") view=Users/>
      <ParentRoute path=path!("/:id") view=User/>
        <Route path=path!("/details") view=Details/>
      </ParentRoute>
    </ParentRoute>
  </Routes>
</Router>

Leaves us with perfectly functional routing, but with no way to refer to any route.

In many parts of our application, we might want to link to some other internal place.
We would have to write these links by hand, leaving all possible compile-time checks on the table.

I wanted some constant value representing a route which can be used both in <Route> declarations as well as in <a>
links or any other place consuming some Segments or anything ToHref.

Creating constants for the values returned by the path macro is cumbersome because of the dynamic types and because
nested route declarations only need the additional path segments specified, these constants would be meaningless without
establishing some parent associations. Only then would we be able to format a full link.

Materializing a link from a list of path segments also requires replacing any dynamic/placeholder segments with concrete
values. The theoretically unlimited number of combinations of segments makes this hard to implement as a trait function.

Therefore, auto-generating structs for your routes, and materialization-function only for the combinations of segments
used seemed alright.

With the above/initially presented routes module, you can write the router declaration as

<Router>
  <Routes>
    <Route path=routes::Root.path() view=Home/>
    <ParentRoute path=routes::Users.path() view=Users/>
      <ParentRoute path=routes:users::User.path() view=User/>
        <Route path=routes::users::user::Details.path() view=Details/>
      </ParentRoute>
    </ParentRoute>
  </Routes>
</Router>

and also create links with the same structs as in

<a href=routes::users::user::Details.materialize("42")>
  "User 42"
</a>

Testing

Run all tests of all creates using the --all flag when in the root directory:

cargo test --all
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants