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

Setting trailing_slash=Strict does not work with nested Routes #2624

Closed
metatoaster opened this issue Jun 11, 2024 · 4 comments
Closed

Setting trailing_slash=Strict does not work with nested Routes #2624

metatoaster opened this issue Jun 11, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@metatoaster
Copy link
Contributor

Describe the bug

It appears the trailing slash improvements provided by #2154 and #2217 still is not quite working as one might expect.

Leptos Dependencies

I've created a fork and built the reproducible code as an example called router_trailing_slash_axum, so the dependency is based directly on the current main branch (or more directly, its Cargo.toml).

To Reproduce

My goal is to have the example app provide an /item/ route that would provide a listing of all items, and individual items can be accessed using a route like /item/1/. I had tried the following definition to take advantage of the nested routes as per documentation.

#[component(transparent)]
pub fn ItemRoutes() -> impl IntoView {
    view! {
        <Route path="/item" view=ItemTop>
            <Route path="/:id/" view=ItemView trailing_slash=TrailingSlash::Exact/>
            <Route path="/" view=ItemListing trailing_slash=TrailingSlash::Exact/>
        </Route>
    }
}

What ended up happening was the route was generated as /item, without the trailing slash, made apparent by the dbg! output generated by the example:

[src/main.rs:13:5] &routes = [
    RouteListing {
        path: "/item/:id/",
        leptos_path: "/item/:id/",
        ...
    },
    RouteListing {
        path: "/item",
        leptos_path: "/item",
        ...
    },
    ...
]

As opposed to path: "/item/", and that only the http://localhost:3000/item route was usable, instead of http://localhost:3000/item/.

I've also tried <Route path="/item/" view=ItemTop trailing_slash=TrailingSlash::Exact> and have the inner route be <Route path="" ...> instead, and that actually made it worse as the /item route no longer functioned. There are a number of other combinations I've tried but none achieved what I really wanted.

Note that the A anchors do work as expected in this example.

Expected behavior

I would have expected the route /item/ be generated. Moreover, I wanted to use Redirect instead, but...

Additional context

This might be related to the axum integration, if Exact is replaced with with Redirect, duplicate routes will be generated for axum. While this is the desired behavior I do plan to have other layers be provided to axum which might be able to mitigate the redirect manually, but this failure to deal with exact trailing slashes is a complete showstopper for what I want to do (as the real application I am building has these specific routes I need working, for example, /path/target.zip should be able to download the zip file, /path/target.zip/ should hit the application to generate a listing of the archive, /path/target.zip/dir/ might list a directory of the archive (or automatically expand the tree at that location), /path/target.zip/dir/file might allow the download of that individual file within the archive.

@metatoaster
Copy link
Contributor Author

I will note that I am now aware of the planned changes in leptos 0.7 will involve refactoring how paths are done. I am wondering if there will be ways to fine-tune control over StaticSegment and ParamSegment for the specific use-case I am after I provided under "Additional context" in my previous post.

@metatoaster
Copy link
Contributor Author

Actually, there is another problem (maybe again with axum) - I am following through this example, following the example with fallback:

<Routes>
  <Route path="/users" view=Users>
    <Route path=":id" view=UserProfile/>
    <Route path="" view=NoUser/>
  </Route>
</Routes>

It seems the NoUser equivalent is never matched in any case, the UserProfile component seems to be receiving :id as a Some("") and the fallback is never used, when I was expecting it to work it never did.

@gbj gbj added the bug Something isn't working label Jun 14, 2024
@metatoaster
Copy link
Contributor Author

Not directly related, but still kind of related as this is still part of the leptos_router package and the consequence of needing trailing slashes causing issues extends to A tags.

The workaround for now that I've used is something like:

<Routes>
  <Route path="/items/" view=ItemListing/>
  <Route path="/items/:id/" view=ItemView/>
</Routes>

I use A tags with active_class something like this inside the <Router>:

<A href="/" active_class="active">"Home"</A>
<A href="/items/" exact=false active_class="active">"Items"</A>

The generated anchor will have the /items/ one have the active_class correctly assigned to class when the location is at /items/, but going to something like /items/123/, the active_class is not set. This most certainly is a separate issue but I am documenting this here for the mean time.

@metatoaster
Copy link
Contributor Author

Okay, now that the 0.7-beta is out and that I have some time to migrate to this, I can confirm that using this version I can create something that works almost exactly how I wanted as nested routes finally behaves in the expected manner. To be a bit verbose and making this comment (and code examples) somewhat self-contained/complete, using the following minimal example under 0.6.13 (nest <ItemRoutes/> into the usual Router/Routes inside some App):

#[component(transparent)]
pub fn ItemRoutes() -> impl IntoView {
    view! {
        <Route path="item" view=|| view! { "pre" <Outlet/> "post" }>
            <Route path="/" view=ItemListing/>
            <Route path="/:id" view=ItemView/>
        </Route>
    }
}

#[component]
fn ItemListing() -> impl IntoView {
    view! {
        <h1>Listing of items</h1>
        <ul>
            <li><A href="/item/1">"Item 1"</A></li>
            <li><A href="/item/2">"Item 2"</A></li>
        </ul>
    }
}

#[component]
fn ItemView() -> impl IntoView {
    let params = use_params_map();
    // this not follow documented syntax either as that doesn't work
    let id = move || {
        params.get().get("id").cloned().unwrap_or_default()
    };
    view! {
        <h1>Viewing item {id}</h1>
    }
}

This shows /item/ resolves into ItemView instead of the desired ItemListing (even though /item will also render ItemListing, that isn't the reported problem). Now the following is the equivalent implementation under 0.7-beta:

#[component]
pub fn ItemRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
    view! {
        <ParentRoute path=StaticSegment("item") view=|| view! { "pre" <Outlet/> "post" }>
            <Route path=StaticSegment("/") view=ItemListing/>
            <Route path=ParamSegment("id") view=ItemView/>
        </ParentRoute>
    }
}
 
#[component]
fn ItemListing() -> impl IntoView {
    view! {
        <h1>Listing of items</h1>
        <ul>
            <li><A href="/item/1">"Item 1"</A></li>
            <li><A href="/item/2">"Item 2"</A></li>
        </ul>
    }
}
 
#[component]
fn ItemView() -> impl IntoView {
    let params = use_params_map();
    let id = move || params.with(|params| params.get("id").clone());
    view! {
        <h1>Viewing item {id}</h1>
    }
}

This works exactly as expected (with the params also working as documented). Moreover, this example that didn't work whatsoever (done so because the nested view didn't work):

view! {
    // ...
    <Route path="/item/:id/" view=ItemView/>
    <Route path="/item/:id/*path" view=ItemPathView/>
    // ...
}

(I ended up removing the top route as the bottom route captures it, and came up with enum based routing like I did with Sauron, which worked, but really not the right way to use Leptos...)

On the other hand, this works within expectations under 0.7-beta:

#[component]
pub fn ItemRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
    view! {
        <ParentRoute path=StaticSegment("item") view=|| view! { "pre" <Outlet/> "post" }>
            <Route path=StaticSegment("/") view=ItemListing/>
            <ParentRoute path=ParamSegment("id") view=|| view! { <Outlet/> }>
                <Route path=StaticSegment("/") view=ItemView/>
                <Route path=WildcardSegment("path") view=ItemPathView/>
            </ParentRoute>
        </ParentRoute>
    }
}

Having the expected route being rendered simply using more types is what I expected of a Rust project. While the whole TrailingSlash feature has been removed I don't think it's a problem now, as the underlying view can potentially control that (maybe trigger a redirect in the ParentRoute's view if no trailing slash is found?). Just having the StaticSegment("/") (or StaticSegment("")) spelling out the empty route has that additional view truly demonstrates the power of nested routing as per chapter 9.2 of the current book.

While the aria-current attribute has a number of bugs and the A tag still needs the absolute path, that is an unrelated issue to this one. With that I think the title of the reported issue is invalidated (as trailing slashes as a concept is removed), and the actual solution I wanted to build is working well in the beta as the expected views are resolved, I feel the issues are resolved and it can just be closed.

This 0.7 release is shaping up to be pretty promising.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants