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

Can a guard function send props to a component? #147

Open
gdamjan opened this issue Oct 8, 2020 · 10 comments
Open

Can a guard function send props to a component? #147

gdamjan opened this issue Oct 8, 2020 · 10 comments
Labels
enhancement New feature or request

Comments

@gdamjan
Copy link
Contributor

gdamjan commented Oct 8, 2020

This is my use-case: I have a guard that checks if the the id for /book/:id is a correct id shape (which can have several shapes), the check function practically normalizes the id, so I'd like to send the normalized id to the component. if the id is not proper, then I show a 404 page. The normalized id is not visible in the url though.

Is it possible to change the props with a guard function?

My current solution is that my component parses the id again, which seems wasteful.

@BogdanDarius
Copy link

You can pass props using userData
Check the documentation on route guards
https://github.com/ItalyPaleAle/svelte-spa-router/blob/master/Advanced%20Usage.md#route-pre-conditions

@gdamjan
Copy link
Contributor Author

gdamjan commented Oct 9, 2020

Thanks @BogdanDarius, I was reading that, but couldn't find the relation between user data and props

@BogdanDarius
Copy link

BogdanDarius commented Oct 9, 2020

Well you can use static props if you want to send a specific id to the component.
In the component have

export let id

and inside the routes

wrap({
  props: { id: 1000 }
})

@ItalyPaleAle
Copy link
Owner

userData is only passed to route guards or events. As for props, I don't believe there's a way to modify them from a route guard that isn't hacky.

Sounds like what you're looking for is a sort of "middleware" and this is not really possible at the moment in the router. And I do think your best option remains to re-do the normalization in the route.

How complicated (computationally intensive) is the function that normalizes the ID?

@gdamjan
Copy link
Contributor Author

gdamjan commented Oct 9, 2020

How complicated (computationally intensive) is the function that normalizes the ID?

I guess not that much, and also it's not running on my CPUs :)

@ItalyPaleAle
Copy link
Owner

and also it's not running on my CPUs :)

I didn't want to say it out loud, but indeed 😁

If you really wanted to avoid re-computing it, I would look into Svelte stores as a possible tool to keep this state. Because only one page can be displayed at a time, it shouldn't be an issue.

Aside from that, I sense an ask here for the ability to pass data from a route guard to a component. In a broader sense, that would be turning route guards into "middlewares". You're the first one to ask for something like this, so I'll keep this open to see if there's others who have a similar need. The good news is that there are workarounds at least: besides re-computing, using "global state" such as Svelte stores should allow this.

@ItalyPaleAle ItalyPaleAle added the enhancement New feature or request label Oct 9, 2020
ItalyPaleAle added a commit that referenced this issue Nov 5, 2020
This allows route pre-condition functions to pass params to components
The proposed API is not final
See #147
@ItalyPaleAle
Copy link
Owner

I've been thinking about this and I have a proposed design for you to consider! I do not consider this final at all, but more like a "request for feedback" 😄

The idea is to introduce the "context". Your route pre-condition functions now receive as this an object that contains the setContext and getContext methods, that allow you to access key-value pairs that are scoped to this specific request.

Each key in the context is then passed to the component's params prop, potentially overriding existing keys.

For example:

routes.js (fragment)

export default {
    '/hello/:first/:last?': wrap({
        component: Name,
        conditions: [
            function (detail) {
                this.setContext('foo', 'bar')
                this.setContext('first', 'mario')
                return true
            },
            function (detail) {
                console.log('foo is', this.getContext('foo'))
                return true
            },
        ],
    })
}

When you visit /hello/alessandro you see in the console foo is bar

Additionally, in the component, params.foo is bar, and params.first is mario (and not alessandro because the context overrode the original value).

Here's the rationale behind this API design:

  1. It should be backwards-compatible
  2. This is an "advanced" thing that a small number of users will need, so it should not make things more complicated for those users that don't need it.

The only downside is that in this case, route pre-condition functions must be defined with the "old" function () {} syntax and you cannot use arrow functions (() => {}), because it's not possible to set a value for this for those.

What do you think? Feel free to play around with the feature/context branch and let me know what you think.

@ItalyPaleAle
Copy link
Owner

Thinking more about it, maybe rather than passing the context's functions through this, I could pass them as second argument to the pre-condition function. It will still maintain backwards-compatibility anyways. And it's easier than having to deal with "this".

@benbucksch
Copy link

benbucksch commented Apr 19, 2021

@ItalyPaleAle The problem is actually more generic, as you already mentioned with the word "middleware".

In my case, my Svelte components all accept real JS objects, not strings. However, the URL is a string, so it has an ID, and I need a function that translates the ID into an object, before calling the component. I do not want this mapping from ID to object to clutter the component, but I want to maintain a clean API for the component, independent from routing. Also all my components will need the same mapping from ID to object.

With "svelte-routing", I can do, and this works:

  <Route path="/person/:objID" let:params>
    <PersonPage person={ storage.getByID(params.objID) } />
  </Route>

and then PersonPage receives a proper person object.

I cannot see a way with svelte-spa-router to achive the same.

I am wondering whether I could just pass a function instead of a component, but I cannot figure out the API for the functions:

const routes = {
  '/person-simple/:id': mapPerson,
  '/person/:id': routeFunc(mapID, PersonPage),
  '/person/:id/delete': routeFunc(mapID, PersonDeletePage),
  '/person/:id/edit': routeFunc(mapID, PersonEditPage),
};

// simple static mapping
async function mapPerson(routeCall) { // API from router to function is unclear
  routeCall.props.obj = storage.getById(routeCall.props.params.id);
  return PersonPage(routeCall.props); // API to pass props to component is unclear
}

// calling generic middleware function
async function routeFunc(middlewareFunc, svelteComponent) {
  return (routeCall) => {
    routeCall.props = middlewareFunc(routeCall.props);
    return svelteComponent;
  };
}

// sample middlewareFunc
function mapID(props) {
  props.obj = storage.getById(props.params.id);
}

@samclaus
Copy link

I would like to add my 2 cents in case it helps. I very much like the look of this library and plan on using it when I eventually migrate the company app from Angular to Svelte.

We do not even have proper routing right now, but in the app I work on, I basically have a ton of custom reactive state. I extensively use key-value caches and subscribable lists of my own making that are like the Svelte store API, but they also pass an event saying if the list of, say, users was refreshed brand-new, or one was added, or one was updated, or one was deleted. This works very well for efficiently recomputing tables and other views as the user clicks around the app and deletes/modifies/creates things. Triggering a refresh, of say, the whole list of users from anywhere in the app updates the information for every user wherever they are currently displayed. We have lots of little information panels, which really should be pages with proper URL routing but are currently just maintained by JS history state, and every type of panel (user, team, drive, etc.) needs to reactively watch the item ID and update information whenever the item is changed from anywhere, or maybe switch to the "404"-esque state when the item is deleted.

The takeaway being, at least in my use case, it is not so simple as "does this item exist when the route is instantiated"--it is more like "watch this item and reflect changes or immediately show a placeholder if it doesn't exist in the first place". In any case, because the final "page" component that is going to be showing the item corresponding to the ID ALREADY needs to subscribe to the ID and reflect changes, we just make every page responsible for showing the placeholder when the item it wants to show doesn't exist. It makes sense to have different placeholders per type of item and not just some generic "redirect to 404 page" route guard thrown in front of every route (we have many types of "things" in the system). This is a huge burden on me because of the amount of similar code, but that is the cost of tailoring UI and making things less generic and crappy for the user. If this sort of thing was to somehow be offloaded onto the router in a generic way, all of the sudden the router would need to know about things like "oh this item got updated, tell the page component" and "unsubscribe from said item when they navigate to a different sort of page".

I think it is best the router simply be responsible for mapping URL->component and facilitate visual transitions between routes and whatnot, but let page components do all the heavy lifting and tell the user if the normal information cannot be displayed for some reason. Abstractions can be used to make similar placeholder code less tedious to share between page components. Your library does cater to single-page-apps which excel in very stateful, snappy, desktop-esque UI in exchange for slower load time thanks to a complex JavaScript code base for managing all that state. These sorts of apps should handle reactivity themselves because they can get arbitrarily complex when it comes to things like offline mode and queueing up lots of requests that may go through at any moment and must update the UI across-the-board when they do succeed.

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

No branches or pull requests

5 participants