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 request: Reset layout inheritance in nested routes #626

Closed
sto3psl opened this issue Jul 26, 2019 · 53 comments · Fixed by #1061
Closed

Feature request: Reset layout inheritance in nested routes #626

sto3psl opened this issue Jul 26, 2019 · 53 comments · Fixed by #1061
Milestone

Comments

@sto3psl
Copy link

sto3psl commented Jul 26, 2019

Is your feature request related to a problem? Please describe.

When building a website I want pages to share a specific layout with common UI elements like a navigation, sidebar, footer, etc. Other pages might want to use different layouts like a custom landing page. This works fine as long as my landing page is not a sub page of an existing page with layout.

** MY SHOP **
src/routes
├── bikes
│   ├── _layout.svelte
│   ├── category
│   │   └── index.svelte
│   └── [slug]
│       ├── index.svelte
│       └── _layout.svelte

In this example /bikes has a custom layout. Individual bike routes like /bikes/fancy-bike-3000 add their own layout and inherit the one from /bikes.

The problem appears when I want to make a super fancy landing page to promote all my mountain bikes. This page is completely individual and uses its own layout.

  ** MY SHOP **
  src/routes
  ├── bikes
  │   ├── _layout.svelte
  │   ├── category
  │   │   ├── index.svelte
+ │   │   └── mountain-bikes-promo
+ │   │       └── index.svelte
  │   └── [slug]
  │       ├── index.svelte
  │       └── _layout.svelte

/bikes/category/mountain-bike-promo will always inherit the layout from /bikes, but in this case it's not wanted.

Sapper needs a feature to bail out of the layout inheritance and let sub pages define their own layouts.

Describe the solution you'd like
I would like an API that allows me as developer to get more flexibility when needed but has the layout inheritance by default.

Ideas:

  • Use a special prop in layout components, that tell Sapper to not include layouts up in the file tree. Maybe something like this:
<!-- _layout.svelte -->
<script>
  export let inherit = false;
</script>
  • Nuxt.js has a layout/ folder with custom layouts that you can specify in your page (https://nuxtjs.org/api/pages-layout). This probably doesn't work well with the current inheritance model but I like it.
/* vue code */
export default {
  layout: 'blog',
  // OR
  layout (context) {
    return 'blog'
  }
}

Describe alternatives you've considered

In the end it's always possible to import layout components like any other component on every page. This provides full flexibility but can be a bit cumbersome if something like the inheritance model already exists.

A different solution could be to structure routes differently but a site structure should ideally not be constrained by the framework of choice.

How important is this feature to you?

I think this is a use case every application of a particular size will eventually run into. May it be custom landing pages or interactive stories with data visualizations.

Additional context

There was a former discussion in this issue sveltejs/sapper#809.
The idea to hide elements in a root layout based on segment doesn't really work since not the full route is available.
Looking at my example bikes/_layout.svelte has segment === "category" but mountain-bike-promos is needed to know if layout components need to be hidden.


I look forward to any API discussion and can maybe come up with an implementation if this is a feature you want to integrate into Sapper.

@zwz
Copy link

zwz commented Aug 4, 2019

I had the same request.
Would it be good that we introduce a reset-layout file __layout.svelte (note the double _), which means it will not inherit layouts up in the file tree, and all the child _layout.svelte will inherit up to it but stops here.

IMO, it is simple, clear, and consistent to the current layout mechanism.

@sto3psl
Copy link
Author

sto3psl commented Aug 16, 2019

@zwz I like the idea, but would reconsider the name. From a quick glance the difference between _ and __ isn't that big and it could look like a typo. It could cause some hard to debug issues where people see their layout not apply (or apply incorrectly) and don't find the issue because the difference between _layout.svelte and __layout.svelte isn't easily visible.

Maybe _root_layout.svelte could be used? This makes it super visible in a file tree, when a new route uses a new layout root. What do you think?

@pngwn
Copy link
Member

pngwn commented Aug 16, 2019

The idea to hide elements in a root layout based on segment doesn't really work since not the full route is available.
Looking at my example bikes/_layout.svelte has segment === "category" but mountain-bike-promos is needed to know if layout components need to be hidden.

That's because that is the segment and not the path (which could be removed in a future version, there is an issue discussing this). If you use the page store, then you have access to the entire path (see the Sapper documentation on stores()).

Regarding this request, this is something that has come up several times. There has been quite a bit of discussion around what the best way is to achieve this. Additional layouts in the relevant route folder would be in-keeping with how Sapper handles things, but it could be confusing having multiple layouts. If you wanted to 'disinherit' a parent layout, you would need to add blank 'reset' layouts, for example. This doesn't feel like an ideal solution to me.

I'm inclined to think that this kind of behaviour is better handled with a config file, but I know that Rich is (or at least has been) against adding a config file to Sapper. I am personally of the opinion that with the amount of CLI options we now have and with feature requests like this, a configuration file should be reconsidered.

One other option is to use a special layouts folder to 'shadow' specific layouts (or vice-versa—define them in a layouts file and shadow them in routes). This could work: custom layouts could be defined by matching the folder location and filename of the file you wish to customise which fits Sapper's routing model, but we would have another special folder. There are probably other problems with this too, what if you only want to 'disinherit' one of many layouts at an arbitrary depth gets weird, I can think of some partial solutions, but the resulting API would be confusing at best and limiting at worst.

If a config file is the best way to go then we need to have a conversation about whether or not we should have a configuration file in Sapper (and some related details) before this issue is tackled.

@sto3psl
Copy link
Author

sto3psl commented Aug 16, 2019

@pngwn could you elaborate how a config file would look like to solve this layout inheritance issue? Would there be a root config with a list of routes that inherit/don't inherit layouts?

For now I will make layout components and import them explicitly until there is a Sapper solution. I think that is what one would do in Next.js.

@pngwn
Copy link
Member

pngwn commented Aug 16, 2019

@pngwn could you elaborate how a config file would look like to solve this layout inheritance issue? Would there be a root config with a list of routes that inherit/don't inherit layouts?

No, I couldn't because I've given it approximately zero thought. But it seems like it might provide a relatively flexible solution.

@carcinocron
Copy link

carcinocron commented Aug 29, 2019

Another idea:

<svelte:layout name="simple" />

or

<svelte:layout inherit={false} />

All the ideas proposed so far work for me.

@sto3psl
Copy link
Author

sto3psl commented Aug 29, 2019

@InstanceOfMichael I don't think this is an issue that should/can be solved in Svelte.

It only concerns how Sapper builds pages by building a Svelte component per page and nesting all _layout.svelte components on the way.
You can see that here https://github.com/sveltejs/sapper/blob/master/src/core/create_manifest_data.ts#L29
and here
https://github.com/sveltejs/sapper/blob/master/src/core/create_app.ts#L271.

If I use Svelte outside of Sapper, there is no concept of layout inheritance.

@khrome83
Copy link

Throwing in my 2 sense...

Would it make sense to return a configuration object as part of the output of preload or another method like preload? That way you have a place in JS where you can make the decision on how a layout needs to behave. Hypothetically.

I like the idea of it being part of the route path as well. But this would give programatic control.

For example a blog post that is part of a series and should inherit the current layout, vs is not part of a series and should not.

@sto3psl
Copy link
Author

sto3psl commented Sep 3, 2019

@khrome83 I'm not sure if that is a possible solution. As I understand it preload get's called at runtime and the pages with all nested layouts are already compiled. That means the solution needs to happen at build time of the page components.

@ramiroaisen
Copy link

I think that the hole layout thing makes no sense, is better if each page imports the layout
I personally use this
//_layout.svelte => <slot></slot>
and have _root.svelte files that the pages can import instead of _layout.svelte
There is no layout in svelte why would be one in sapper?

@timeshift92
Copy link

Hello everyone, but maybe you’ll make a type like this person’s, he added _reset.svelte ?
https://github.com/jakobrosenberg/svelte-filerouter

@Saiv46
Copy link

Saiv46 commented Feb 27, 2020

Any progress?

@jokin
Copy link

jokin commented Mar 3, 2020

I found the routify (https://routify.dev/ ) approach very straightforward, if you have a _reset.svelte file on a folder, you use that and stop traversing the filesystem.

I don't get by there a 2 projects with so many similarities instead of one, it's a hard decision to make a choice between booth, I'm evaluating what to use for a very constrained device (a tv with a low end SOC), and using svelte instead of angular has bring a lot of improvement to the speed of the interface, but having to decide between so many options on routing is tiresome

@joelhickok
Copy link

I really like the _reset.svelte approach.

I am amazed Sapper does not yet have the ability to use multiple layouts.

@joycollector
Copy link

joycollector commented Apr 1, 2020

Created a PR with _reset.svelte implementation: sveltejs/sapper#1141

@arxpoetica
Copy link
Member

I actually quite like the _reset.svelte idea. It's a pretty simple convention. Can we call it _layout.reset.svelte or something like that?

@arxpoetica
Copy link
Member

@joycollector it appears there may need to be some more thought put into a solution for this.

As @pngwn pointed out:

but it could be confusing having multiple layouts. If you wanted to 'disinherit' a parent layout, you would need to add blank 'reset' layouts, for example. This doesn't feel like an ideal solution to me.

Likewise, what if one is nested three deep and wants to re-inherit the root layout. As much as I ::shudder:: at the thought of a config file to solve this, sadly 🐧 might be right.

Let's keep digging on solutions until we can hit something that answers all the right questions.

@mindrones
Copy link
Member

I'd favour the _reset approach over the config file one as it's visual and simple to manage.

@antony
Copy link
Member

antony commented May 13, 2020

A few things:

  • I think that the fact that _layout.reset.svelte stops all the middle-hierarchy but still inherits the root, is a bit odd (non-obvious). Additionally, the only places I've ever wanted to do this, are places where I've not wanted the root layout.
  • Having a separate, empty svelte file to reset the layout seems like a bit of a hack.
  • In reference to my previous point, I'd feel much better about it if _layout.reset.svelte was actually the new layout, and just indicated that the layout didn't inherit from it's ancestors. That might be what we're discussing, but I'm not sure.

What about if layout files had an (optional) layout name:

root/
├── _layout.svelte
├── index.svelte
├── subdir/
│       ├── _layout.svelte // inherits from root
│       ├── index.svelte

and then to manage inheritance:

root/
├── _layout.a.svelte
├── _layout.b.svelte
├── index.svelte
├── subdir/
│       ├── _layout.a.svelte // inherits from a layout
│       ├── index.svelte
│       ├── /somedir
│       │       ├── _layout.b.svelte
│       │       ├── index.svelte // uses layout in same dir, and inherits from b layout (skips a)
│       ├── /otherdir
│       │       ├── index.svelte // uses a layout a

This might be harder to express, however, but something along those lines would allow your layout choice not to depend on hierarchy.

@arxpoetica
Copy link
Member

@antony mmmm...interesting idea there, but I can poke holes in it. Which layout is the default? What if you want to use a particular layout, but don't need to change it—does that mean just putting an empty file with the same name in the nested folder? Seems an odd convention.

At first I liked the idea until I saw these holes. I am wondering though if this is closer...

@antony
Copy link
Member

antony commented May 15, 2020

@arxpoetica my intention was (and I didn't express it as I didn't want to muddy the waters), that it would work the same as it does now. The _layout.svelte would remain the default, and if it doesn't exist, there would be no layout (similar to how it is now). That gives you the flexibility of an automatic layout reset.

The behaviour of sub-directories would be similar. It's not unusual to have (in file-system based routing), a layout file which simply defines <slot></slot> to inherit a layout from a layer above. It's the same penalty you pay when using js files to duplicate content across URLs. It's a factor of file-system based routing - which I don't especially like, personally, but see the advantage of, for most applications.

@kevmodrome
Copy link

I quite like the Routify way of doing it, just a _reset.svelte file in the folder you want to stop the inheritance in: https://github.com/sveltech/routify-starter/tree/master/src/pages/example/reset

Might it be a good idea to make the API cross-over as much as possible between the two projects? Maybe it's not something that's possible to do though.

@DaFrenchFrog
Copy link

DaFrenchFrog commented May 19, 2020

Hi there, I am only learning sapper and svelte right now and I was browsing the web for a "sapper change layout", to discover the discussion is quite actual.
My contribution is quite probably stupid as I am an amateur developer, but as a professional UX designer I would have felt quite "natural" that any _layout.svelte file inside a child route folder would automatically reset the parent layout. Is it stupid ?
I was thinking such thing would be perfect as I want to change the background-color of the body.
Anyway... Keep up the awesome work !!

@grundmanise
Copy link

@elromano it's not stupid IMO, for me it would also make sense if layouts wouldn't add up. We would be able to create different layout components and use them as desired in _layout.svelte. That way layout inheritance would be opt-in.

@gokayokyay
Copy link

I think it should be easier but I did following for a personal project:

{#if segment === 'profile'}
  <slot />
{:else}
  <div>
  .... main layout
{/if}

if the segment is profile it renders slot but the default is still my main layout, therefore I can use custom _layout.svelte for that segment.

@willparsons
Copy link

Routify handles this problem really well with their _reset.svelte files. I'm currently trying to set up a login page which will completely differ from the standard global layout of the site and it's looking to be quite difficult in sapper.

@Florian-Schoenherr
Copy link

@arxpoetica

Likewise, what if one is nested three deep and wants to re-inherit the root layout.

Is this an actual concern? I don't really think so, but could be wrong.
Also, if this is really needed, people can still have a root layout of

<MyLayout>
  <slot/>
</MyLayout>

then just use _layout.reset on some nested elements and three deep do

<MyLayout>
  <slot/>
</MyLayout>

again.

@babakfp
Copy link

babakfp commented Feb 20, 2021

It's funny that there is no solution for such a simple issue. I'm decided to rebuild my app with Nuxt.js (Vue.js) or maybe React. This wasn't what you told us Mr @Rich-Harris .
"Oh Svelte is a compiler and we can add so much stuff built-in for the developers so they don't worry about building them they own. Yet we don't have a proper layout system."

@Florian-Schoenherr
Copy link

@babakfp attitude + take a look at routify if you need extreme routing capabilities "right now or else"

@frederikhors
Copy link
Contributor

@babakfp what a pity your tone.

@Rich-Harris
Copy link
Member

The route manifest is generated purely based on the files inside src/routes. If you had files like this...

src/routes/$layout.svelte
src/routes/foo/$layout.svelte
src/routes/foo/bar/$layout.svelte
src/routes/foo/bar/baz/$layout.svelte
src/routes/foo/bar/baz/index.svelte

then your route manifest would look like this:

import * as layout from "../../../src/routes/$layout.svelte";

const components = [
  () => import("../../../src/routes/foo/$layout.svelte"),
  () => import("../../../src/routes/foo/bar/$layout.svelte"),
  () => import("../../../src/routes/foo/bar/baz/$layout.svelte"),
  () => import("../../../src/routes/foo/bar/baz/index.svelte")
];

export const routes = [
  // src/routes/foo/bar/baz/index.svelte
  [/^\/foo\/bar\/baz$/, [components[0], components[1], components[2], components[3]]]
];

export { layout };

In other words you need to import all those components. That's true regardless of whether one of those layouts has the reset export, meaning the earlier imports are wasted. By contrast, if you use the filename, you can trivially do this:

src/routes/$layout.svelte
src/routes/foo/$layout.svelte
src/routes/foo/bar/$layout.svelte
-src/routes/foo/bar/baz/$layout.svelte
+src/routes/foo/bar/baz/$layout.reset.svelte
src/routes/foo/bar/baz/index.svelte
import * as layout from "../../../src/routes/$layout.svelte";

const components = [
-  () => import("../../../src/routes/foo/$layout.svelte"),
-  () => import("../../../src/routes/foo/bar/$layout.svelte"),
-  () => import("../../../src/routes/foo/bar/baz/$layout.svelte"),
+  () => import("../../../src/routes/foo/bar/baz/$layout.reset.svelte"),
  () => import("../../../src/routes/foo/bar/baz/index.svelte")
];

export const routes = [
  // src/routes/foo/bar/baz/index.svelte
-  [/^\/foo\/bar\/baz$/, [components[0], components[1], components[2], components[3]]]
+  [/^\/foo\/bar\/baz$/, [components[0], components[1]]]
];

export { layout };

(I'm glossing over the fact that right now the root layout is always included; we'd need to start explicitly including it in route definitions if we started having resets. Which is totally fine as it would actually simplify some stuff.)

@GrygrFlzr
Copy link
Member

Interesting! You can also use $layout.reset.svelte with the /login use case anyway, so this seems like a good way to handle it.

@dummdidumm
Copy link
Member

Would it be possible then to have one layout in-between have it reset, but everything below will be able to opt in to all parent's layouts again? Like a $layout.restore.svelte?

@Florian-Schoenherr
Copy link

@dummdidumm what's the usecase? (I already asked this somewhere in this thread)
I genuinely want to know, because I can't think of any route layout where you go down the resource-tree, a layout gets removed and deeper it gets restored.
You could also do

<MyLayout>
  <slot/>
</MyLayout>

on root $layout.svelte and on the deeper nested $layout.reset.svelte.

If it's no implementation overhead, of course it would be cool.

@seanlail
Copy link
Contributor

seanlail commented Apr 2, 2021

@Florian-Schoenherr A use case I have found is a kind of "summary" page in a long journey.

You would want the layout all the way down but somehow exclude it for that page and then be included afterwards.

At the moment I just toggle components in my layout page based on the page path, but that requires anyone updating my "summary" page to be aware that they need to go up the tree and update the layout if they want to make changes.

@Florian-Schoenherr
Copy link

Florian-Schoenherr commented Apr 2, 2021

@seanlail 😕
In $layout.reset.svelte you would have the advantage of just putting some kind of wrapper there again, without toggling.
What's the usecase for $layout.restore.svelte, can anyone point me at a live project which resets and also restores layout? (+with too much overhead due to this not being available)

@seanlail
Copy link
Contributor

seanlail commented Apr 2, 2021

@Florian-Schoenherr very true. That does make it simpler. I guess then the restore part is left? Would the reset only work in that specific route and not cascade?

@Rich-Harris Rich-Harris mentioned this issue Apr 3, 2021
5 tasks
@Rich-Harris Rich-Harris added this to the 1.0 milestone Apr 3, 2021
Rich-Harris added a commit that referenced this issue Apr 3, 2021
Rich-Harris pushed a commit that referenced this issue Apr 5, 2021
* load all components up-front

* typechecking

* invoke router on startup if no SSR

* getting closer

* tests passing

* remove trial script

* fix unsubscription

* lint

* rename some stuff
Rich-Harris pushed a commit that referenced this issue Apr 5, 2021
* create separate _load_error method

* make error component available to renderer

* include root layout in route manifests

* note to self

* allow unspecified adapter

* gah whoops

* smarter chunking

* fix chunking

* update tests
Rich-Harris added a commit that referenced this issue Apr 6, 2021
@Rich-Harris Rich-Harris mentioned this issue Apr 6, 2021
5 tasks
Rich-Harris added a commit that referenced this issue Apr 11, 2021
@babakfp
Copy link

babakfp commented Apr 12, 2021

For those, like myself, coming across this issue with newer release of SvelteKit, this seems to be working well for me:

<script lang="ts">
	import { page } from '$app/stores';
	import Default from '$lib/layouts/Default.svelte';
	import Alternative from '$lib/layouts/Alternative.svelte';

	export let layout = Default;

	const layouts = {
		'/': Default,
		'/other': Alternative
	};

	page.subscribe(({ path }) => {
		layout = layouts[path] ?? Default;
	});
</script>

<svelte:component this={layout}>
	<slot />
</svelte:component>

Note, I made use of the $lib prefix and stored my layouts there (./src/lib/layouts)

What about /shop/? Your layout system will break if the user manually writes the URL like example.com/blog/. This / in the last of the URL is a common thing so, HEADSHOT :)

@babakfp
Copy link

babakfp commented Apr 12, 2021

Hi

@Rich-Harris
Can't we just create the layout system like how it is in the NuxtJS? What if I want to change the layout if the x was greater than the y? This is not possible with the current layout system. So it means that SvelteKit isn't 100% reactive because the app can't react in some circumstances, right? (hmm emoji).


@SillyFreak
Your solution was great at the time but now I'm facing an issue. Check out this repo please

@Florian-Schoenherr
Copy link

@babakfp layouts[path] ?? layouts[path+"/"] ?? Default; don't know how ts works but something like that

Rich-Harris pushed a commit that referenced this issue Apr 12, 2021
* failing test for #626

* update create_manifest_data and unit tests

* update types

* get existing tests working

* WIP

* nested error pages working in SSR, pass error/status via load

* most tests passing

* make it possible to filter tests

* all tests passing except the new one

* refactor some stuff, fix types

* fix/simplify caching

* refactoring and stuff

* remove unused property

* bypass _load on initial hydration

* pass entire route to this._load

* fix caching

* it works

* lint/check

* document a and b

* explain what layout_stack and error_stack are

* slight touch-up

* document nested error pages

* changeset
@hitrust
Copy link

hitrust commented Apr 14, 2021

I’m just a newbie.
Maybe SvelteKit can use jinja2's template pattern. Then no need to reset layout and even to inherit it. just import the right layout using the right layout‘s path. It's more flexible and explicit.

src/routes/$layout.svelte
src/routes/index.svelte
src/routes/foo/$layout.svelte
src/routes/foo/index.svelte
src/routes/foo/bar/index.svelte

<---src/routes/index.svelte---->
import ‘$layout.svelte’

<---src/routes/foo/index.svelte---->
import ‘foo/$layout.svelte’

<---src/routes/foo/bar/index.svelte---->
import ‘$layout.svelte’

Rich-Harris added a commit that referenced this issue Apr 17, 2021
Rich-Harris added a commit that referenced this issue Apr 17, 2021
@Rich-Harris Rich-Harris mentioned this issue Apr 17, 2021
5 tasks
Rich-Harris pushed a commit that referenced this issue Apr 17, 2021
* failing test for #626

* layout resets - closes #626

* docs

* improve docs
@Florian-Schoenherr
Copy link

A Huge thanks! @Rich-Harris 🎉 🎉

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

Successfully merging a pull request may close this issue.