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

Feature: routed modals + parameters in modal routes #121

Open
lauri865 opened this issue Aug 24, 2023 · 10 comments
Open

Feature: routed modals + parameters in modal routes #121

lauri865 opened this issue Aug 24, 2023 · 10 comments

Comments

@lauri865
Copy link

lauri865 commented Aug 24, 2023

Loving this package so far. I generally prefer to have modals routed though rather than state based in order to have shareable links as a default, rather than state-based (which are only good for global routes).

I have managed to do this myself with custom routes file:
E.g., I have a route in pages/companies/+edit.[id].tsx

And then in routes file I do this to render modals for routes, following the internal matching engine of react-router-dom:

const modalsArray = Object.entries(modalRoutes).map(([path, Component]) => ({
  path: path
    .replace(...patterns.route)
    .replace(...patterns.splat)
    .replace(...patterns.param)
    .replace(/(^|\/)\-\:(:?[\w-]+)(\/|$)/, '$1$2?'),
  element: <Component />,
}))

export const Modals = () => {
  const location = useLocation()
  const matches = matchRoutes(modalsArray, location)
  return renderMatches(matches)
}

Would be great to have this with type-safety out of the box for linkable routes with params.

@oedotme
Copy link
Owner

oedotme commented Aug 24, 2023

That's interesting! Could you share that via StackBlitz for example? I'll be happy to have a look into that.

I was also thinking recently to store current modal as a search params instead of route state to have it shareable.

@lauri865
Copy link
Author

lauri865 commented Aug 25, 2023

I can set on up later. But after more exploration, it seems like the best way to add routed modals would be to add them as children to the parent, and rendering through an <Outlet /> in the parent. Then you can also easily close them by using navigate("..") to route back to the parent.

Check out this example from the official repo: https://github.com/remix-run/react-router/blob/dev/examples/modal-route-with-outlet/src/App.tsx

That would also support cases for routed tabs, etc. I don't think there's a convention to add nested routes right now, is there? Maybe something like index+modal.tsx?

@lauri865
Copy link
Author

lauri865 commented Aug 25, 2023

After more testing, seems as if nesting works with, e.g. this structure:

  • companies (directory)
    • index.tsx
    • [id].tsx
    • [id] (directory)
      • tab1.tsx
      • tab2.tsx

The only problem is what if I want the index page to also have child routes? That doesn't seem to be possible. The only option is to define company.tsx in root pages folder, but that makes everything in the company directory a descendant of that.

I think a good option would be to treat an index directory as a source for children for the index route. Right now if you would create an index directory in e.g. companies, then it would create an erroneous route group with path "/", and that's not valid:
react-dom.development.js:26923 Uncaught Error: Absolute route path "/" nested under path "/companies" is not valid. An absolute child route path must start with the combined path of all its parent routes.

Update:
There's a small bug with this structure though, it generates duplicate type for Params:

export type Params = {
  '/companies/:id': { id: string }
  '/companies/:id': { id: string }

@oedotme
Copy link
Owner

oedotme commented Aug 25, 2023

I think this example https://github.com/oedotme/generouted/tree/main/examples/react-location/modals might be using a similar approach to what you're referring to.

Basically to have an outlet and a child modal component, which gets mounted on top of it's parent.

If I recall correctly it had some limitations and it's wouldn't be global by default. It worked tho while I was testing that, and I guess it can be used right now with nested layouts.

Currently the global modals work well for me, but I'm interested to explore ways to improve it.

@archiekd
Copy link

I am actually trying to do this right now. Is this something you would be interested in implementing? @oedotme

I am trying to do the following:

Kanban board at project/123

click on a card on the board and then a draw appears with the card information at project/123/card/456 however it looks like something like this solution would allow me to do that where as I am not sure at the moment the best way to do this with genrouted?

@oedotme
Copy link
Owner

oedotme commented Sep 15, 2023

@archiekd modal routes can be achieved with route layouts + certain structure + just links and can be done with or without generouted. It's different than the existing global modals in generouted though, however you can combine both methods in the same application for different types of modals.

I added an example for react-router with route modals. In this example, /gallery is a layout route at pages/gallery.tsx with an <Outlet /> component. The modal route /gallery/:id at /pages/gallery/[id].tsx, is basically a "modal" or a "drawer" component that covers up the parent layout.

Here's the online example on StackBlitz

image image

Hope that helps!

@lauri865
Copy link
Author

I managed to get it working mostly on my end without major modifications to generouted, but there's one use case still not properly supported. There's no way to add child routes to the index page.

Say you have routes for a tabbed view:

  • /pages/gallery.tsx (parent route, holding common UI for all tabs)
  • /pages/gallery/index.tsx (default tab)
  • /pages/gallery/tab1.tsx
  • /pages/gallery/tab2.tsx

Say I want to add a modal to the gallery page/tab and render the tab still in the background. For other tabs it's easy – add a folder [tab1] and e.g. file edit.tsx within there (+outlet in the tab). There's no intuitive way to achieve the same for the index page though.

gallery/index.tsx is interpreted as an index: true path by default which cannot have any children. Adding a folder called index creates an erroneous path "/" which cannot exist at all as a nested path. I have monkeypatched it on my end with custom routes, by merging routes with erroneous path: "/" as children of index: true route on the same level.

@suiramdev
Copy link

suiramdev commented Oct 12, 2023

All of your suggestions seem very delicate to me. I don't really understand if we are all trying to achieve the same goal, but I would rather explain my point of view on the current flows of the modal system.

In the context of my application, modals are sometimes shared between routes: I'd like to have a modal that deletes an item based on a given ID. To achieve this, I can do the following for a route: (dashboard)/items/[id]/+delete.tsx. This would work to delete my item in that modal, based on the id parameter in the URL, but if I'm in (dashboard)/items/index.tsx there's no way for the modal to get the id parameter.

In fact, your tricky way would work by setting a route page that is actually a modal (using the <Outlet />). But isn't that confusing, since _layout.tsx usually uses it? See what I mean? A route shouldn't act as a layout and it shouldn't act as a modal.

Solution :
A solution, but probably costly, would be to have some sort of context for each modal, so we can set them parameters without depending on the route URL.

I like the idea of giving modal parameters in their filename too, like you gave in the example: +modal.[id].tsx, but for having more than one parameter will make the filename loooong.

@suiramdev
Copy link

suiramdev commented Oct 12, 2023

So I found a strange way to deal with modal parameters on arbitrary routes by using history state :

(dashboard)/products/[id]/+delete.tsx

function DeleteProductModal() {
  let { id } = useParams("/products/:id");
  if (!id) id = history.state.usr.product.id;

(...)
}

(dashboard)/products/_components/ProductsTable.tsx

(...)
              <DropdownMenuItem
                className="hover:!bg-destructive hover:!text-destructive-foreground"
                onClick={() => modals.open("/products/[id]/delete", { state: { product: { id: product.id } } })}
              >
                <TrashIcon className="mr-2 h-4 w-4" />
                Delete
              </DropdownMenuItem>
(...)

Perhaps this is an interesting idea to include in generouted? Maybe use the URL parameters, or else look in the history state?

@lauri865
Copy link
Author

lauri865 commented Oct 12, 2023

Well, what you're trying to achieve is a confirm box rather than a modal. And there's no reason to put it in a route to begin with – it's not like you want or need a shareable link for a confirm dialog (imagine a situation where deletion fails for some reason, if all else fails you'd rather want the user to get rid of it with a hard refresh rather than meeting the same bug over again). I think you're much better of just having a reusable confirm dialog component that defines the the ID on the trigger. You'll find it much easier to implement and don't have to have any hacky workarounds with IDs.

To that tune, I also personally don't think that global modals that don't have a URL should be handled by a routing library – they don't have a route, do they?! Also, they may need additional state and if the state doesn't live in the URL, they become finicky very easily. In that case you're also better off having reusable modal component for global modals that are tied to their triggers rather than awkwardly living in an additional routing tree / history.state.

Radix has good implementations for both that help you appreciate the paradigm:
https://www.radix-ui.com/primitives/docs/components/alert-dialog
https://www.radix-ui.com/primitives/docs/components/dialog

I would love to avoid having to add individual outlets for routed dialogs (albeit there's nothing particularly wrong with that approach, as you can have nested Outlets for many things, such as tabs, and other content(1) – it's just the additional boilerplate that bothers me, as modals are often placed in a portal anyways), but I'm yet to come to a better alternative which enables to properly store the state in and retrieve the state from the URL, without creating alternative navigation logic on top of useNavigate and <Link to="url". And not using history.state, which is not persistent, so I try to avoid that, as then you can easily end up in a situation where you have a shareable link for a modal, which depends on state from the history, but when it's actually shared, it doesn't work, and you need to implement additional logic to handle such cases. Or worse yet, if the modal doesn't have a URL yet the open state is stored in history.state, then you wouldn't expect the modal to stay open on a refresh, but it does.

--
(1) - https://remix.run/ website demo illustrates a good example of outlet based composition for inspiration.

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

No branches or pull requests

4 participants