diff --git a/code/renderers/svelte/src/decorators.ts b/code/renderers/svelte/src/decorators.ts index abf9d80b7a31..07fb1753b56d 100644 --- a/code/renderers/svelte/src/decorators.ts +++ b/code/renderers/svelte/src/decorators.ts @@ -1,78 +1,84 @@ import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types'; import { sanitizeStoryContextUpdate } from '@storybook/preview-api'; -import SlotDecorator from '../templates/SlotDecorator.svelte'; +// ! DO NOT change this SlotDecorator import to a relative path, it will break it. +// ! A relative import will be compiled at build time, and Svelte will be unable to +// ! render the component together with the user's Svelte components +// ! importing from @storybook/svelte will make sure that it is compiled at runtime +// ! with the same bundle as the user's Svelte components +// eslint-disable-next-line import/no-extraneous-dependencies +import SlotDecorator from '@storybook/svelte/templates/SlotDecorator.svelte'; import type { SvelteRenderer } from './types'; /** - * Check if an object is a svelte component. - * @param obj Object - */ -function isSvelteComponent(obj: any) { - return obj.prototype && obj.prototype.$destroy !== undefined; -} - -/** - * Handle component loaded with esm or cjs. + * Handle component loaded with ESM or CJS, + * by getting the 'default' property of the object if it exists. * @param obj object */ -function unWrap(obj: any) { - return obj && obj.default ? obj.default : obj; +function unWrap<T>(obj: { default: T } | T): T { + return obj && typeof obj === 'object' && 'default' in obj ? obj.default : obj; } /** - * Transform a story to be compatible with the PreviewRender component. + * Prepare a story to be compatible with the PreviewRender component. * - * - `() => MyComponent` is translated to `() => ({ Component: MyComponent })` - * - `() => ({})` is translated to `() => ({ Component: <from context.component> })` - * - A decorator component is wrapped with SlotDecorator. The decorated component is inject through - * a <slot/> + * - `() => ({ Component: MyComponent, props: ...})` is already prepared, kept as-is + * - `() => MyComponent` is transformed to `() => ({ Component: MyComponent })` + * - `() => ({})` is transformed to component from context with `() => ({ Component: context.component })` + * - A decorator component is wrapped with SlotDecorator, injecting the decorated component in a <slot /> * * @param context StoryContext * @param story the current story - * @param originalStory the story decorated by the current story + * @param innerStory the story decorated by the current story */ -function prepareStory(context: StoryContext<SvelteRenderer>, story: any, originalStory?: any) { - let result = unWrap(story); - if (isSvelteComponent(result)) { - // wrap the component - result = { - Component: result, +function prepareStory( + context: StoryContext<SvelteRenderer>, + rawStory: SvelteRenderer['storyResult'], + rawInnerStory?: SvelteRenderer['storyResult'] +) { + const story = unWrap(rawStory); + const innerStory = rawInnerStory && unWrap(rawInnerStory); + + let preparedStory; + + if (!story || Object.keys(story).length === 0) { + // story is empty or an empty object, use the component from the context + preparedStory = { + Component: context.component, + }; + } else if (story.Component) { + // the story is already prepared + preparedStory = story; + } else { + // we must assume that the story is a Svelte component + preparedStory = { + Component: story, }; } - if (originalStory) { - // inject the new story as a wrapper of the original story - result = { + if (innerStory) { + // render a SlotDecorator with innerStory as its regular component, + // and the prepared story as the decorating component + return { Component: SlotDecorator, props: { - decorator: unWrap(result.Component), - decoratorProps: result.props, - component: unWrap(originalStory.Component), - props: originalStory.props, - on: originalStory.on, + // inner stories will already have been prepared, keep as is + ...innerStory, + decorator: preparedStory, }, }; - } else { - let cpn = result.Component; - if (!cpn) { - // if the component is not defined, get it the context - cpn = context.component; - } - result.Component = unWrap(cpn); } - return result; + + return preparedStory; } export function decorateStory(storyFn: any, decorators: any[]) { return decorators.reduce( - ( - previousStoryFn: LegacyStoryFn<SvelteRenderer>, - decorator: DecoratorFunction<SvelteRenderer> - ) => + (decorated: LegacyStoryFn<SvelteRenderer>, decorator: DecoratorFunction<SvelteRenderer>) => (context: StoryContext<SvelteRenderer>) => { - let story; - const decoratedStory = decorator((update) => { - story = previousStoryFn({ + let story: SvelteRenderer['storyResult'] | undefined; + + const decoratedStory: SvelteRenderer['storyResult'] = decorator((update) => { + story = decorated({ ...context, ...sanitizeStoryContextUpdate(update), }); @@ -80,10 +86,10 @@ export function decorateStory(storyFn: any, decorators: any[]) { }, context); if (!story) { - story = previousStoryFn(context); + story = decorated(context); } - if (!decoratedStory || decoratedStory === story) { + if (decoratedStory === story) { return story; } diff --git a/code/renderers/svelte/src/render.ts b/code/renderers/svelte/src/render.ts index ad111dcf14c7..777997c09598 100644 --- a/code/renderers/svelte/src/render.ts +++ b/code/renderers/svelte/src/render.ts @@ -1,7 +1,11 @@ /* eslint-disable no-param-reassign */ import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { SvelteComponentTyped } from 'svelte'; - +// ! DO NOT change this PreviewRender import to a relative path, it will break it. +// ! A relative import will be compiled at build time, and Svelte will be unable to +// ! render the component together with the user's Svelte components +// ! importing from @storybook/svelte will make sure that it is compiled at runtime +// ! with the same bundle as the user's Svelte components // eslint-disable-next-line import/no-extraneous-dependencies import PreviewRender from '@storybook/svelte/templates/PreviewRender.svelte'; diff --git a/code/renderers/svelte/src/types.ts b/code/renderers/svelte/src/types.ts index 23e76243f702..e30b2e8f549d 100644 --- a/code/renderers/svelte/src/types.ts +++ b/code/renderers/svelte/src/types.ts @@ -48,4 +48,5 @@ export interface SvelteStoryResult< ? Record<string, (event: CustomEvent) => void> : { [K in keyof Events as string extends K ? never : K]?: (event: Events[K]) => void }; props?: Props; + decorator?: ComponentType<Props>; } diff --git a/code/renderers/svelte/template/stories/slot-decorators.stories.js b/code/renderers/svelte/template/stories/slot-decorators.stories.js new file mode 100644 index 000000000000..759904403640 --- /dev/null +++ b/code/renderers/svelte/template/stories/slot-decorators.stories.js @@ -0,0 +1,42 @@ +import ButtonView from './views/ButtonView.svelte'; +import BorderDecoratorRed from './views/BorderDecoratorRed.svelte'; +import BorderDecoratorBlue from './views/BorderDecoratorBlue.svelte'; +import BorderDecoratorProps from './views/BorderDecoratorProps.svelte'; + +export default { + component: ButtonView, + decorators: [() => BorderDecoratorRed], + args: { + primary: true, + }, +}; + +export const WithDefaultRedBorder = {}; +export const WithBareBlueBorder = { + decorators: [() => BorderDecoratorBlue], +}; +export const WithPreparedBlueBorder = { + decorators: [ + () => ({ + Component: BorderDecoratorBlue, + }), + ], +}; +export const WithPropsBasedBorder = { + decorators: [ + () => ({ + Component: BorderDecoratorProps, + props: { color: 'green' }, + }), + ], +}; +export const WithArgsBasedBorderUnset = { + argTypes: { + color: { control: 'color' }, + }, + decorators: [(_, { args }) => ({ Component: BorderDecoratorProps, props: args })], +}; +export const WithArgsBasedBorder = { + ...WithArgsBasedBorderUnset, + args: { color: 'lightblue' }, +}; diff --git a/code/renderers/svelte/template/stories/svelte-mdx.stories.mdx b/code/renderers/svelte/template/stories/svelte-mdx.stories.mdx index 9d8fb482aa47..b8b3cabb51b0 100644 --- a/code/renderers/svelte/template/stories/svelte-mdx.stories.mdx +++ b/code/renderers/svelte/template/stories/svelte-mdx.stories.mdx @@ -1,6 +1,7 @@ import globalThis from 'global'; import { Meta, Story, Canvas } from '@storybook/addon-docs'; import ButtonView from './views/ButtonView.svelte'; +import BorderDecoratorRed from './views/BorderDecoratorRed.svelte'; <Meta title="stories/renderers/svelte/svelte-mdx" /> @@ -33,3 +34,15 @@ import ButtonView from './views/ButtonView.svelte'; }} </Story> </Canvas> + +<Canvas> + <Story name="WithDecorator" decorators={[() => BorderDecoratorRed]}> + {{ + Component: ButtonView, + props: { + primary: false, + text: 'Secondary text', + }, + }} + </Story> +</Canvas> diff --git a/code/renderers/svelte/template/stories/views/BorderDecoratorBlue.svelte b/code/renderers/svelte/template/stories/views/BorderDecoratorBlue.svelte new file mode 100644 index 000000000000..e0bb0db72e2f --- /dev/null +++ b/code/renderers/svelte/template/stories/views/BorderDecoratorBlue.svelte @@ -0,0 +1,3 @@ +<div style="border: 3px solid blue; padding: 10px; margin: 10px;"> + <slot /> +</div> diff --git a/code/renderers/svelte/template/stories/views/BorderDecoratorProps.svelte b/code/renderers/svelte/template/stories/views/BorderDecoratorProps.svelte new file mode 100644 index 000000000000..53fdd7c54f4a --- /dev/null +++ b/code/renderers/svelte/template/stories/views/BorderDecoratorProps.svelte @@ -0,0 +1,7 @@ +<script> + export let color = 'violet'; +</script> + +<div style="border: 3px solid {color}; padding: 10px; margin: 10px;"> + <slot /> +</div> diff --git a/code/renderers/svelte/template/stories/views/BorderDecoratorRed.svelte b/code/renderers/svelte/template/stories/views/BorderDecoratorRed.svelte new file mode 100644 index 000000000000..6c3f33e49a14 --- /dev/null +++ b/code/renderers/svelte/template/stories/views/BorderDecoratorRed.svelte @@ -0,0 +1,3 @@ +<div style="border: 3px solid red; padding: 10px; margin: 10px;"> + <slot /> +</div> diff --git a/code/renderers/svelte/templates/PreviewRender.svelte b/code/renderers/svelte/templates/PreviewRender.svelte index 6ad197bea0f0..7747cd8fcaba 100644 --- a/code/renderers/svelte/templates/PreviewRender.svelte +++ b/code/renderers/svelte/templates/PreviewRender.svelte @@ -15,12 +15,10 @@ props = {}, /** @type {{[string]: () => {}}} Attach svelte event handlers */ on, - Wrapper, - WrapperData = {}, } = storyFn(); // reactive, re-render on storyFn change - $: ({ Component, props = {}, on, Wrapper, WrapperData = {} } = storyFn()); + $: ({ Component, props = {}, on } = storyFn()); const eventsFromArgTypes = Object.fromEntries( Object.entries(storyContext.argTypes) @@ -28,24 +26,16 @@ .map(([k, v]) => [v.action, props[k]]) ); - const events = { ...eventsFromArgTypes, ...on }; - if (!Component) { showError({ title: `Expecting a Svelte component from the story: "${name}" of "${kind}".`, description: dedent` Did you forget to return the Svelte component configuration from the story? - Use "() => ({ Component: YourComponent, data: {} })" + Use "() => ({ Component: YourComponent, props: {} })" when defining the story. `, }); } </script> -<SlotDecorator - decorator={Wrapper} - decoratorProps={WrapperData} - component={Component} - {props} - on={events} -/> +<SlotDecorator {Component} {props} on={{ ...eventsFromArgTypes, ...on }} /> diff --git a/code/renderers/svelte/templates/SlotDecorator.svelte b/code/renderers/svelte/templates/SlotDecorator.svelte index 6b0d06337001..5bd0fdfe3455 100644 --- a/code/renderers/svelte/templates/SlotDecorator.svelte +++ b/code/renderers/svelte/templates/SlotDecorator.svelte @@ -1,10 +1,10 @@ <script> import { onMount } from 'svelte'; - export let decorator; - export let decoratorProps = {}; - export let component; + + export let decorator = undefined; + export let Component; export let props = {}; - export let on; + export let on = undefined; let instance; let decoratorInstance; @@ -23,9 +23,9 @@ </script> {#if decorator} - <svelte:component this={decorator} {...decoratorProps} bind:this={decoratorInstance}> - <svelte:component this={component} {...props} bind:this={instance} /> + <svelte:component this={decorator.Component} {...decorator.props} bind:this={decoratorInstance}> + <svelte:component this={Component} {...props} bind:this={instance} /> </svelte:component> {:else} - <svelte:component this={component} {...props} bind:this={instance} /> + <svelte:component this={Component} {...props} bind:this={instance} /> {/if}