sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.66k stars 4.13k forks source link

Share $props between components #10809

Closed KevsRepos closed 6 months ago

KevsRepos commented 6 months ago

Describe the problem

Often enough you have a set of components which all belong to the same domain, meaning they share a lot of structure and their props. Threlte is definitely one of the bigger examples, but usually every component library has this issue.

The Threlte Maintainer found a way to share props between components by inventing an inheritance model for Svelte components. <Foo.Bar /> where Foo defines a base set of props that automatically apply to Bar.

As far as I know, there is no such concept in Svelte. Maybe we dont even have to implement it. I just wanted to give a real life example where a lot of effort had to be put in to solve the critical problem of ever repeating prop definitions across multiple components.

Many component libraries often share "base" props between every component. Whether its a <Button />, <Input /> or a <Card /> - all of them need a class prop to extend styles, or maybe a border, rounded or expand prop.

As far as I know, the only method to accomplish this, is to manually define every prop individually on every component. And dare a prop changes or is added, you can iterate over every component again.

Describe the proposed solution

One method to solve the props issue:

export const shared = {children, x, y, z, foo} = $props.definition();
<script>
import { shared } from "$lib/shared.svelte.js";

const myProps = $props.use(shared);
</script>

{@render myProps.children()}

Less repetitive and still flexible.

Importance

would make my life easier

Rich-Harris commented 6 months ago

This exists, no?

<script lang="ts">
  import type { Props } from './common';

  let { x, y, z, ...rest }: Props = $props();
</script>
KevsRepos commented 6 months ago

This exists, no?

<script lang="ts">
  import type { Props } from './common';

  let { x, y, z, ...rest }: Props = $props();
</script>

Importing the type works but only when using typescript or jsdoc at least. Also, it just makes sure you stick to the type definition, but you still have to repeat the entire declaration of your props in every component.

After experimenting with $props() I found out you can create something like an "abstract" component and use composition: https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA5WR3U7EIBCFX2VCvNhNGnpfsf49hnixpTQSy09gutmV8O4WabfGxI1eDWfIx5w5RDKoUQbSvERiDlqShjw6RyqCZ5dFOMoR5ayDnbzIHRaEVw5bbjgq7axHeJoQrYHBWw2c0LpoWlhObrlh9UYZVu7b51GJd9CSLUA7j9G2V4OSPWnQTzJVm6suoD8I_Ks1YU1AiPk4izc19l6aqsjTUs9L_cglwR3cOG9d2O1_Oo4PM9xLD-tD93S3T9fslo3-meO645bk2vme5WU3oJR-GYZr3llXws0gu0yIK5ugbjPR_fYFr-kTjUovrCQCAAA=

But it doesnt play well with types and especially not with snippet types.

dummdidumm commented 6 months ago

What do you mean by "it doesn't play well with types and especially not snippet types"?

In general, I don't see how this will make anyone's lives easier, nor will it solve use cases more elegantly.

Rich-Harris commented 6 months ago

Yeah, I'm having a real hard time understanding why this...

const x = $props.use(shared);

...is better than this:

const { ...x } = $props();

And what is shared? In the example above...

export const shared = {children, x, y, z, foo} = $props.definition();

children, x, y, z and foo must refer to declarations in scope otherwise all you've got is a bunch of reference errors. So are people actually supposed to do this?

let children;
let x;
let y;
let z;
let foo;
export const shared = {children, x, y, z, foo} = $props.definition();

It all seems a bit confusing to me.

7nik commented 6 months ago

Do you want something like this?

export type SharedUtilsTypes = {
  class?: string,
  border?: string,
  rounded?: number,
  expanded?: boolean,
}
export function sharedAction(elem, { class: className, border, rounded, expanded }: SharedUtilsTypes) {
  // some shared code
}
export function helperFunction({ class: className, border, rounded, expanded }: SharedUtilsTypes) {
 // another shared code
return ...
}
<script lang="ts">
  import { SharedUtilsTypes, sharedAction, helperFunction } from "./sharedUtils.ts";
  type MyTypes = SharedUtilTypes & { 
    x: number,
    y: string,
    z?: boolean,
  }
  let { x, y, z, ...sharedProps }: MyTypes = $props();

  let foo = helperFunction(sharedProps);
</script>

<div use:sharedAction={sharedProps}>
 some content
</div>
dummdidumm commented 6 months ago

Given that there are various sensible solutions to different use cases and because I can't imagine this getting easier with a separate rune, I'm going to close this

KevsRepos commented 6 months ago

There is one problem with Snippets and typescript though. https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA52SPU_DMBCG_8rJYmilKNlDW-jGiMSIGUJyAUuJbdmXqsjyf8d2YrWUCgGbz76P5319jvViQMvqZ8dkMyKr2V5rVjD60DGwBxwIQ2zVZNp4s7GtEZpgaOTbljOynO24FKNWhuABmw4N9EaNwFlZzXE5d-HslstNNdeHmhAs-dUuTBhVJ3qBHavJTOiLE9CrJdO09BuqVknCIwWw0G4aMMHhMcGJ8GT6pkXIHR-N0hZcQKH2XQydQVnDkxRaIxVc-kvcq9I5LeIjHLhcDz7bcKaeU5qlpKV57GlwEUNfX7Bt4UbHw2r9zbtOHNJ0dx-qo4u50125Wif0mPGTsbP7__7sTFoE0Un7F3R_2oJ8f74HXC4uQFmWSSL8RXxOBJfL_dUtevGf_kQeReICAAA=

While the solution with the abstract component works in principle, it is impossible to properly type snippets when using this pattern. When you use the Snippet type from the svelte package, ts will yell that

 Property 'children' is missing in type '{}' but required in type 'AbstractProps'

When trying to allow nullification (children?: Snippet, {@render children?.()}), we get

Argument of type '(unique symbol & { _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; }) | undefined' is not assignable to parameter of type 'unique symbol & { _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; }'.
  Type 'undefined' is not assignable to type 'unique symbol & { _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; }'.
    Type 'undefined' is not assignable to type 'unique symbol'.

Maybe this problem deserves its own issue?

dummdidumm commented 6 months ago

The nullification approach is the right solution - there was a bug within language tools that caused this type error which was fixed yesterday, make sure you're using the latest version of svelte-check and the VS Code extension