Closed mrkishi closed 6 years ago
It might be a little tricky because of preload
. Currently, each top-level component (aka page) gets to load asynchronous dependencies before it's rendered (either on server or client) β non-top-level components don't have that ability (similar to Next's getInitialProps
).
I've wondered about having some way to preserve components between route transitions:
<Nav key:nav/> <!-- because of key:nav, the component is 'recycled', never destroyed -->
Having a main app component would solve that problem in a slightly more elegant way, but I think we'd need to solve the async loading problem before we could really do that. (Perhaps we need to steal some of these ideas from React.)
I agree that there should be a way to listen for route changes.
In my app, I also preload route data.
What I do is app.set({ route: Component, data: preloaded })
, but I could see a fancier API for cases where the main app component would intercept the routing logic.
I guess what I proposed here is not changing the way routes
work, but rather init
-- making some of sapper's framework logic more... Svelty, for lack of better word.
(ps. thanks for the link! I'll check it out)
Ah, I think I understand what you mean now. Yes, that could potentially work. Will think on that. Definitely needs spread in order to be ergonomic.
Something like this would be great. Currently this is a blocker for us using Sapper for a work project. We need nested routes where components are preserved between routes. For example one section of the app has a Google map and an overlay component that controls the route and updates the map's state, currently if we used Sapper each route change would teardown and refresh the map.
I think for Sapper we need something like Nuxt.js and Next.js. We need a Layout
component that has a special <sapper />
component inside of it, anything in this Layout
component would not be destroyed on route change, only components that gets loaded in the <sapper />
component would be destroyed.
I think it would be simpler to use dynamic components β to update the original example to v2 syntax, and throw in some layout:
<Nav {route}/>
<svelte:component this={Page} {query} {params}/>
<footer>
<p>this appears on every page</p>
</footer>
The act of navigation, as far as the client-side router goes, would look something like this (pseudo-code):
const route = selectRoute(window.location);
const { Page } = route;
if (Page.preload) {
await Page.preload.call(preloadContext, {
query: route.query,
params: route.params
});
}
if (redirected || erroredInPreload) handleThatShit();
app.set({
route: new Route(route), // wrapper that provides convenience methods like 'matches', maybe
Page,
query: route.query,
params: route.params
});
We could extend that idea and solve a common problem declaratively (e.g. #248):
+{#if preloading}
+ <FakeProgressBar/>
+{/if}
<Nav {route}/>
<svelte:component this={Page} {query} {params}/>
<footer>
<p>this appears on every page</p>
</footer>
if (Page.preload) {
+ app.set({ preloading: true });
await Page.preload.call(preloadContext, {
query: route.query,
params: route.params
});
}
// ...
app.set({
+ preloading: false
route: new Route(route), // wrapper that provides convenience methods like 'matches', maybe
Page,
query: route.query,
params: route.params
});
It might turn out we don't actually need hooks, and can just use the familiar on('state', ...)
mechanism. At the very least, I would try this approach first, and see if there's still demand for nav hooks later.
Bikeshedding time: what do we call this new app component? app/App.html
seems a bit odd. Maybe app/Main.html
?
Oh, and if a Store
has been registered then maybe those top-level values should be set there as well, so some deeply nested component could show a loading spinner when preloading
is true or something
That could possibly work. We could call the new component app/Layout.html
.
I suppose in the spirit of 'less magic', you could make it explicit, so that it doesn't really matter what you call it and app/Layout.html
(which I like) is just convention:
import { init } from 'sapper/runtime.js';
import { routes } from './manifest/client.js';
import Layout from './Layout.html';
init({
target: document.querySelector('#sapper')
routes,
component: Layout
});
Awesome, if we could have it working by morning (Sydney time) that'll be grand. π
Jokes aside, this sounds promising. Would this technique work for deeply nested pages?
@rob-balfre I think all nested components would be destroyed on route change. I think only the Layout component would remain.
@Rich-Harris yeah I like the idea of making it less magic-y. Explicit always better than implicit in most cases.
@rob-balfre maybe morning Sydney time on Monday? (I actually think this should be relatively straightforward to implement, and I'm impatient, so gonna try and do it over the weekend.)
What do you mean by deeply nested pages exactly?
@silentworks is right that only the Layout component would remain... though I've been thinking about this issue a little. Right now, the way transitions work is that if you create a component, its intros will run, but if you destroy it then outros don't run β they're destroyed immediately. That makes it tricky, if not impossible, to achieve many transition effects.
As a starting point, it would be useful if it were possible to outro components, and in fact there's an issue for it: https://github.com/sveltejs/svelte/issues/1211. I think I have an idea of how to solve it.
That wouldn't help if you had a single <svelte:component>
, because it only has one this
value at a time... but I think we could maybe address that with a new directive:
<svelte:component switch:outin this={Page} {query} {params}/>
(outin
would mean 'outro old component, then intro new component' β there could be a few different values, like 'destroy old component immediately then intro new one', or 'intro new component while old one is outroing', etc.)
Essentially I want it to be dead simple to do this sort of thing, and this feels like a first step towards that.
Also, one more thought I had: if we had a value that indicated not just the route
we're currently on, but the intended route β i.e., if someone is on 'about' and clicks the 'blog' button in the nav, we're stuck in preload waiting for 'blog.json' to load, then our route
is about
but our intended route is blog
β so that we can do optimistic UI updates like switching the selection in the nav to make the switch feel more instantaneous. Not sure what a suitable concise name for 'intended route' is.
@Rich-Harris the last part of your message is the reason why I think having id
and type
on the client side routes will be useful.
I am not too sure about adding an directive on the <svelte:component>
to define behaviour. I am still trying to work out how Vue is doing it with its transition component. Because in Nuxt you can easily hook into the transition from a css class name.
@Rich-Harris here is an example of how Vue is working this with its <transition>
component. Which I think is a more elegant way.
https://vuejs.org/v2/guide/transitions.html#Transitioning-Between-Components
@Rich-Harris π₯Monday is a deal! I'll buy you a coffee/beer if you ever come to Sydney!
By deeply nested I mean anything more than just one level deep. A basic example:
@rob-balfre Ah, gotcha. Kind of. If you had a page like routes/[menu]/[submenu].html
, and no routes/[menu].html
(or routes/profile.html
or whatever) then that page wouldn't change if you navigated between /profile/notifications
and /profile/account
. Instead, the existing component would get new parameters.
So if that page looked like this...
<TopLevelNav active={params.menu}/>
{#if params.menu === 'home'}
<Home {params}/>
{:else if params.menu === 'profile'}
<Profile {params}/>
{:else if params.menu === 'settings'}
<Settings {params}/>
{/if}
...then navigating would basically just change the value of params
on the existing <Profile>
component.
More likely, you'd have something like routes/profile/[submenu].html
and similar pages for 'home' and 'settings' β in which case the submenu would be updated in place on navigating from /profile/notifications
to /profile/account
, but some DOM would get unnecessarily trashed on navigating from /profile/whatever
to /settings/whatever
.
Hope that made sense.
Ok, here's a brainteaser. What should the default scroll behaviour be when navigating to the 'same page' but with different params
/query
? If you were navigating from /profile/notifications
to /profile/account
you would probably expect the scroll position to stay the same, but if you were navigating from /news/1
to /news/2
or /blog/first-post
to /blog/second-post
you would expect to jump back to the top of the page.
I would scroll to the top even on the same page with same params, since that's the default browser behavior when clicking plain-old links.
Just think about what you'd expect when clicking links in a site's footer. Would /profile/account
do nothing just because you're at the bottom of your account page?
Have opened a PR, if anyone is curious: https://github.com/sveltejs/sapper/pull/259
@mrkishi yeah, I came to the same conclusion. It also happens to be the option that doesn't require any extra work :)
This is released in 0.12. Migration guide here. Thanks everyone!
@Rich-Harris brilliant, thanks so much.
Would someone be able to add a working example of this somewhere? in the Svelte REPL maybe? I'm assuming we can do something like <svelte:component switch:outin this={Page} {...props}/>
but then where do we define the outin switch behaviour? I'd really like to be able to contribute back to the Sapper project with a bunch of solid working route/component transitions similar to the example you gave in the Ember conf, or the vuejs/nuxt_js example at https://css-tricks.com/native-like-animations-for-page-transitions-on-the-web/ & https://page-transitions.com/
Just read through the sveltejs Gitter and it seems you're already aware of the page-transitions example, so that's cool
So, I've been using pure Svelte and Roadtrip in order to build an app with an architecture similar to sapper. I wanted to use sapper but it was just too early... Thankfully this allowed me to test different things that might benefit sapper.
One of them is having a main app component. The simplest (and implicitly used if one's not provided) main app component would be something like this:
The framework would then define an API for the main app component in order to expose the routed components (although the minimal API would be simply "there's a
routeComponent
component and arouteData
object").This would allow us to have persistent components throughout the whole app lifecycle. It would also be a good place to expand sapper's routing API (which is something I think is currently lacking -- say, getting notified whenever routes change), with full Svelte access.
This would probably only be ergonomic once spread parameters are in, but hopefully it's worth tinkering with!
Thoughts?