Skip to content

Commit

Permalink
Merge pull request #19987 from storybookjs/jeppe/fix-svelte-decorator…
Browse files Browse the repository at this point in the history
…s-slots

Svelte: Fix decorators with slots
  • Loading branch information
JReinhold authored Nov 30, 2022
2 parents 96eb473 + 942e339 commit 2cb1e88
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 70 deletions.
104 changes: 55 additions & 49 deletions code/renderers/svelte/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,95 @@
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),
});
return story;
}, context);

if (!story) {
story = previousStoryFn(context);
story = decorated(context);
}

if (!decoratedStory || decoratedStory === story) {
if (decoratedStory === story) {
return story;
}

Expand Down
6 changes: 5 additions & 1 deletion code/renderers/svelte/src/render.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
1 change: 1 addition & 0 deletions code/renderers/svelte/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
42 changes: 42 additions & 0 deletions code/renderers/svelte/template/stories/slot-decorators.stories.js
Original file line number Diff line number Diff line change
@@ -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' },
};
13 changes: 13 additions & 0 deletions code/renderers/svelte/template/stories/svelte-mdx.stories.mdx
Original file line number Diff line number Diff line change
@@ -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" />

Expand Down Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div style="border: 3px solid blue; padding: 10px; margin: 10px;">
<slot />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
export let color = 'violet';
</script>

<div style="border: 3px solid {color}; padding: 10px; margin: 10px;">
<slot />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div style="border: 3px solid red; padding: 10px; margin: 10px;">
<slot />
</div>
16 changes: 3 additions & 13 deletions code/renderers/svelte/templates/PreviewRender.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,27 @@
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)
.filter(([k, v]) => v.action && props[k] != null)
.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 }} />
14 changes: 7 additions & 7 deletions code/renderers/svelte/templates/SlotDecorator.svelte
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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}

0 comments on commit 2cb1e88

Please sign in to comment.