Replies: 1 comment 1 reply
-
Paraphrased feedback from someone:
|
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
While working with the addon recently, I discovered a few areas where our support for Svelte snippets is suboptimal, and I think we can improve them. I'm listing them all out here because I think they are all related, but in essence we could approve/decline and implement each idea individually if we want to.
I think doing these would truly make snippets first class citizens of Storybook, which would be a great win for the community.
Throughout the RFC we'll be working with this Layout component, which relies heavily on snippets:
Layout.svelte
and in an application it would be used like this:
App.svelte
Playground: https://svelte.dev/playground/2dd31039c3144aa9b987072ea245b3de?version=5.28.2
Problem 1: Snippets can't be args
Args in stories is a great construct, because it allows you to easily set them per-story. Args are also what powers the Controls panel, making it easy for users to try out their components with different args.
The problem is, that Controls can only really work with primitive-like values - strings, numbers, objects, booleans, etc. - and snippets are functions. So while you can set an arg to be a snippet, the control for it would be a stringified function which is both confusing and unusable.
If you try and set a snippet like the
header
-prop to just be a primitive arg like'My header content'
, Svelte will fail because it is expecting the prop to be a snippet-function, not a plain string.The workaround today is that users define a template that converts the plain-string arg to a snippet:
Proposal A: Convert args to snippets automatically
We could convert specific args to snippets automatically behind the scenes, so users wouldn't have to do it manually with a verbose template.
But how? We can't know upfront which args should be snippets, and which should be left as-is. You might be tempted to use our automatic argTypes inference to find all props typed as Snippet, but we should not let argTypes control how stories render. ArgTypes are purely for documentation and manager purposes, and a story should render the exact same if argTypes was defined or not.
This wasn't always the case, but we've seen that argTypes generation slows down building and rendering a lot, so we're moving towards an architecture where argTypes inference and handling is lazy, not blocking the story rendering pipeline.
This also rules out the user specifying which args should be treated as args in the
argTypes
definition.Also, this is highly specific to Svelte, and the story annotations API (
title
,args
,argTypes
,decorators
, etc.) is framework agnostic, so we shouldn't expand that API with Svelte-specific stuff.I propose that we add a second
options
argument todefineMeta
, that is specific to Svelte CSF. Down the line this could be expanded to configure more things in Svelte CSF, but right now it could contain a singlesnippetize
(naming feedback welcome), that users would use to tell Storybook which args to convert to snippets:snippetize
could default to being{ children: true }
- and a customsnippetize
should merge with that - becausechildren
is sort of a reserved word in Svelte: https://svelte.dev/docs/svelte/snippet#Passing-snippets-to-components-Implicit-children-snippetHowever you can override that by setting
{ children: false }
. if you have a component that supports<MyComp children="plain string" />
. We could also say we don't support that, because it is an anti-pattern and Svelte docs state:an alternative API would be an array,
snippetize: ['header', 'children', 'footer']
, however the merging-heuristics for that is less obvious to me and to users.A full solution would look like this:
Two wins:
children
andfooter
to snippets, because they are already snippets in the{...args}
spread.Unresolved questions
snippetize
isn't part of the story annotations, it won't automatically follow Storybook's inheritance-model withpreview.js
->meta
->story
. However I doubt it makes sense to globally define which prop names to snippetize inpreview.js
, as it would be a per-component thing. Similarly I doubt there is a use case for setting it at a story-level - you wouldn't have a component that supports a prop both being a snippet and a primitive at the same time, would you? Maybe I'm missing something.snippetize
, it means that the arg you set indefineMeta
can now be a primitive and doesn't need to match theSnippet
type derived from the component's prop types. And when you define a custom template that takes inargs
, it needs to understand that those args are now snippets, not primitives. A simpler solution would be to always convert allSnippet
-types to beSnippet | AllThePrimitives
(I tried this once but failed, I'm sure it's doable though).Proposal B: Automatically convert
children
arg to snippetOnly and always* convert the
children
arg to a snippet. An argument is that named snippets are niche, and so requiring a bit of gymnastics to make them work is okay. But we know thatchildren
should always be a snippet, so we can convert that safely. This means we don't need thesnippetize
-API at all, and in general makes this a lot easier to implement.*: Unless it is already a snippet. booleans probably also should not be converted, in reality only
string
andnumber
makes sense to snippetize.Problem 2: Story children cannot be used with templates
In #228 we introduced the feature that a
Story
'schildren
would be forwarded aschildren
to the component:This made it easier to write stories with
children
snippets, however it came with one limitation: you can't mix Storychildren
withtemplate
. This is not allowed:But it doesn't have to be that way, we can support this!
Proposal: Merge Story
children
intoargs
What we could do, is instead of making it a completely separate rendering case when
Story
haschildren
, we just merge theStory
-component'schildren
into the args, overriding the existingargs.children
if it's there.It's not a huge win, but it is actually easier to maintain for us, and it's one less thing to know as a user, it simplifies the mental model: "
children
onStory
is a shortcut forargs.children
, because writingargs.children
manually with a snippet is cumbersome".FYI if users were to do this manually today (which they shouldn't), they would achieve it like this:
Again, we need to ensure that the code snippet generation logic can support this case
Problem 3: Passing snippets requires a template
Continuing from the Problem 2 above, why isn't this supported for any snippets in a
Story
? You can't pass any snippet to aStory
and expect it to be forwarded, onlychildren
. You can't do this today:Although it seems like a natural mental model to have from a user perspective: "any snippet passed to
Story
(including implicitchildren
) will be set asargs
on the story, ie. forwarded to the component or template".You might think that this is unnecessary if Problem 1 is solved, however Problem 1 is about turning primitve args into snippets, while the use case for this problem is when you have complex snippets that can't be expressed with simple strings, ie. because they contain other components or Svelte constructs, so they need to be snippets.
Proposal: Merge any snippets passed to
Story
intoargs
Expand the idea of Problem 2, to not just cover
children
, but any snippet passed toStory
will merge intoargs
, overriding any existing arg with the same name. However there are a few gotchas:Firstly, Story already accepts a
template
-snippet. It is a reserved keyword in a sense, it won't be forwarded to args obviously. We also can't forward story annotations likeplay
orbeforeEach
, because you'd get props collisions if you try to define both a prop and a snippet with that name. Should you have a component that accepts a reserved keyword snippet itself, the workaround is to use that directly (as it is the case already today):Secondly, Svelte currently doesn't expose an API that allows us to check if something is a snippet or not (See sveltejs/svelte#14017, sveltejs/svelte#9774 and sveltejs/svelte#9903), it seems like the maintainers don't think the currently known use cases are valid or perhaps an anti-pattern. Maybe this will change that, I don't know. I've heard they are great people.
For us it means we can only check if any given prop to
Story
is a function, which doesn't necessarily mean it is a snippet. Many story annotations are functions, eg. this shouldn't become a snippet:<Story play={() => {...}} />
. There are a few solutions to this:toString
. This is fragile if the internals ever change in a Svelte version.const IGNORED_PROPS = ['play', 'beforeEach', 'decorators', ...]
. This is fragile if Storybook decides to change story annotations in the future, ie. in the near futureexperimental_afterEach
could be renamed toafterEach
, which we would need to handle. However this is within our control, so easier to maintain.If we implemented this proposal, it would make the above Just Work™:
Beta Was this translation helpful? Give feedback.
All reactions