Closed daKmoR closed 1 year ago
Rendering and navigations are decoupled; the navigation always happens (because thats how the web works, I'm not aware of any router that allows you to cancel the navigation entirely and just have nothing happen when a link is clicked, and I also don't think you should, it feels very un-web-like). But you can still show a global spinner, and only render the new page when the promise resolves by awaiting the promise in the route-changed
event. The actual navigation would still happen, though, but I think it should. I think it's weird to click an anchor and then nothing happens.
Alternatively you can try using the shouldNavigate
hook for this, pseudocode, but something like:
export function resolveData(promise) {
return {
name: 'resolveData',
shouldNavigate: (context) => ({
condition: async () => {
let result = false;
try {
context.data = await promise();
result = true;
} catch {
result = false;
}
return result;
},
redirect: '/client-list',
})
}
}
I was totally confused as I saw it working in the playground but I could not get it to work in my codebase...
but I think I have narrowed down the issue... even with shouldNavigate it still changes the this.route
which makes sense... but if then any other event triggers a rerender of the main app... (like a loading indicator)
then it will render the new route even though shouldNavigate is not yet done and then depending on your template it may error or will need to show some loading state (as data is not yet available)
you can see it in action here
so if there is an external update then rendering and navigation seems to not be decoupled 😅 so maybe my original code idea is still the best way forward? 🤔
what do you think?
so if there is an external update then rendering and navigation seems to not be decoupled 😅
Well it is decoupled, it just depends on when and where you're calling the render function. (And in this case how Lit works as well, because this.route
is updated sync before shouldNavigate
updates this.route
)
even with shouldNavigate it still changes the this.route
This is a fair point though, but this could be remedied by moving execution of the shouldNavigate
hook to before assigning the new route, which I think makes more sense actually. Currently, we assign the new route (this.route
) synchronously, then execute shouldNavigate
which is async and gives lit time to render (and this.route
is now already the new route, so it'll be re-rendered by lit), and may also re-assign the route, which is why I think you're seeing the route rendered. It's kind of weird to already assign the route if we dont even know if we shouldNavigate
there or not.
If I understand correctly, this is the expected behavior you want to see, correct?
https://github.com/thepassle/app-tools/assets/17054057/3578fe93-6a69-4dff-893b-716043a0bd1d
If so, we can change the assigning of this.route
to move after shouldNavigate
has happened:
--- a/router/index.js
+++ b/router/index.js
@@ -165,13 +165,13 @@ export class Router extends EventTarget {
url = new URL(url, this.baseUrl);
}
- this.route = this._matchRoute(url) || this._matchRoute(this.fallback);
+ let route = this._matchRoute(url) || this._matchRoute(this.fallback);
log(`Navigating to ${url.pathname}${url.search}`, { context: this.context, route: this.route });
/** @type {Plugin[]} */
const plugins = [
...(this.config?.plugins ?? []),
- ...(this.route?.plugins ?? []),
+ ...(route?.plugins ?? []),
];
for (const plugin of plugins) {
@@ -181,7 +181,7 @@ export class Router extends EventTarget {
const condition = await result.condition();
if (!condition) {
url = new URL(result.redirect, this.baseUrl);
- this.route = this._matchRoute(url) || this._matchRoute(this.fallback);
+ route = this._matchRoute(url) || this._matchRoute(this.fallback);
log('Redirecting', { context: this.context, route: this.route });
}
}
@@ -191,6 +191,8 @@ export class Router extends EventTarget {
}
}
+ this.route = route;
^ I think something like this should work
yes that is the behaviour I would like to get 👍
tried the change but it seems to bring another "problem" e.g. if lit rerenders while the route changes then it will rerender the "old" route - but somehow the old context.data
is no longer available 🤷
so now the "new" route works as expected (e.g. wait until data is there and then render)... but the "old" route has missing data.
e.g. that only applies if old and new route both depend on data (which is probably often the case)
See example here with the changes to the Router.js (new file) from above
PS: this also highlight that for the initial rendering (from the app side) the contex.data will always be empty 😅 e.g. you need a condition in the render to account for that... would be nice if that won't be needed
tried the change but it seems to bring another "problem" e.g. if lit rerenders while the route changes then it will rerender the "old" route - but somehow the old context.data is no longer available 🤷
Right, because if a redirect (a new assignment of the route) happens in shouldNavigate
it wont execute the shouldNavigate
hook for the route
thats being redirected to.
So then if a redirect has happened, we may have to run the shouldNavigate
hook for that route, too
huh - there is no redirect involved? at least not intentional 🤔 if I remove all redirects
inside shouldNavigate it still behaves the same...
or do you mean any route change is a "redirect"?
Maybe I'm misunderstanding then, can you clarify? Why is context.data
empty?
I don't know why it's empty... if it would be there then it would work as expected 😅
in the Playground Link
if you click on John
then it renders the "loading message" of the hello page... which is strange as that page is currently displayed and already has it's data loaded....
so somehow on the navigation it resets the context maybe? I can maybe do some debugging this evening but right now I'm confused 😅
Right, so:
this.navigate
happensthis.navigate
we do a new this._matchRoute
, which creates a new this.context
for the route we're navigating to (item/:item
)document.querySelector('simple-greeting').loading
, that triggers a new render, so the route.render
is called for the Hello
route, but the context has already changedYou can avoid the rerendering that happens when the loading state is set by assigning the result of route.render()
to a property on the class:
static properties = {
loading: { type: Boolean },
+ route: {}
};
firstUpdated() {
router.addEventListener('route-changed', () => {
- this.requestUpdate();
+ this.route = router.render();
});
}
render() {
return html`
${this.loading ? html`Global Loading...` : ''}
- ${router.render()}
+ ${this.route}
`;
}
Then it should only call the render callback for a route whenever it has actually changed:
https://github.com/thepassle/app-tools/assets/17054057/50827df9-7981-480a-8a3e-e9a1585353db
sweet - that works 💪
also less rerendering of the route 👍
Fixed in 0.9.7
, and published :)
updated and works 🎉
always a pleasure doing business with you 🤗
I would like to fully load an api request before doing the actual rendering/navigation.
what should happen
what happens
ideas
I tried the
beforeNavigation
but that instantly renders and results in thewhat happens
outputhow about adding a
beforeRender
hook? then the above code will work fine with one line changecode idea
this.route
could be split intothis.renderRoute
andthis.route
andrenderRoute
would only be updated after allbeforeRender
hook have been executed.usage example
what do you think?