From 038a20520c4b1c1d45e5465c52b6f7db970910cf Mon Sep 17 00:00:00 2001 From: j3rem1e Date: Mon, 1 Feb 2021 19:02:58 +0100 Subject: [PATCH] Simplify Svelte decorators --- addons/docs/src/frameworks/svelte/HOC.svelte | 6 +- .../src/frameworks/svelte/prepareForInline.ts | 11 +- .../storyshots/storyshots-core/package.json | 3 +- .../src/frameworks/svelte/renderTree.ts | 4 + .../src/client/preview/PreviewRender.svelte | 37 +++++++ .../src/client/preview/SlotDecorator.svelte | 30 ++++++ app/svelte/src/client/preview/decorators.ts | 100 +++++++++++++++++ app/svelte/src/client/preview/index.ts | 3 +- app/svelte/src/client/preview/render.ts | 102 ++---------------- app/svelte/src/typings.d.ts | 1 + .../snippets/svelte/button-group-story.js.mdx | 1 - .../button-story-component-decorator.js.mdx | 26 +---- .../svelte/button-story-decorator.js.mdx | 12 +-- docs/snippets/svelte/button-story.mdx.mdx | 3 +- docs/snippets/svelte/page-story.js.mdx | 2 +- .../storybook-preview-global-decorator.js.mdx | 12 +-- .../your-component-with-decorator.js.mdx | 26 +---- docs/snippets/svelte/your-component.js.mdx | 1 - .../src/components/Context.svelte | 9 ++ .../src/stories/BorderDecorator.svelte | 15 +++ .../decorators.stories.storyshot | 27 +++++ .../src/stories/decorators.stories.js | 24 +++++ 22 files changed, 274 insertions(+), 181 deletions(-) create mode 100644 app/svelte/src/client/preview/PreviewRender.svelte create mode 100644 app/svelte/src/client/preview/SlotDecorator.svelte create mode 100644 app/svelte/src/client/preview/decorators.ts create mode 100644 examples/svelte-kitchen-sink/src/components/Context.svelte create mode 100644 examples/svelte-kitchen-sink/src/stories/BorderDecorator.svelte create mode 100644 examples/svelte-kitchen-sink/src/stories/__snapshots__/decorators.stories.storyshot create mode 100644 examples/svelte-kitchen-sink/src/stories/decorators.stories.js diff --git a/addons/docs/src/frameworks/svelte/HOC.svelte b/addons/docs/src/frameworks/svelte/HOC.svelte index e7138bea429..0b6c3618701 100644 --- a/addons/docs/src/frameworks/svelte/HOC.svelte +++ b/addons/docs/src/frameworks/svelte/HOC.svelte @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/addons/docs/src/frameworks/svelte/prepareForInline.ts b/addons/docs/src/frameworks/svelte/prepareForInline.ts index 5496bf45a1e..4d5b6640363 100644 --- a/addons/docs/src/frameworks/svelte/prepareForInline.ts +++ b/addons/docs/src/frameworks/svelte/prepareForInline.ts @@ -1,22 +1,17 @@ -import { StoryFn, StoryContext } from '@storybook/addons'; +import { StoryFn } from '@storybook/addons'; import React from 'react'; // @ts-ignore import HOC from './HOC.svelte'; -export const prepareForInline = (storyFn: StoryFn, context: StoryContext) => { - // @ts-ignore - const story: { Component: any; props: any } = storyFn(); +export const prepareForInline = (storyFn: StoryFn) => { const el = React.useRef(null); React.useEffect(() => { const root = new HOC({ target: el.current, props: { - component: story.Component, - context, - props: story.props, - slot: story.Component, + storyFn, }, }); return () => root.$destroy(); diff --git a/addons/storyshots/storyshots-core/package.json b/addons/storyshots/storyshots-core/package.json index f8bbe9e12de..27fc41939f7 100644 --- a/addons/storyshots/storyshots-core/package.json +++ b/addons/storyshots/storyshots-core/package.json @@ -82,7 +82,8 @@ "react-dom": "^16.8.0 || ^17.0.0", "rxjs": "*", "vue": "*", - "vue-jest": "*" + "vue-jest": "*", + "svelte": "*" }, "peerDependenciesMeta": { "@storybook/angular": { diff --git a/addons/storyshots/storyshots-core/src/frameworks/svelte/renderTree.ts b/addons/storyshots/storyshots-core/src/frameworks/svelte/renderTree.ts index 74d6b3953d6..d3a16e792a0 100644 --- a/addons/storyshots/storyshots-core/src/frameworks/svelte/renderTree.ts +++ b/addons/storyshots/storyshots-core/src/frameworks/svelte/renderTree.ts @@ -1,4 +1,5 @@ import { document } from 'global'; +import { set_current_component } from 'svelte/internal'; /** * Provides functionality to convert your raw story to the resulting markup. @@ -12,6 +13,9 @@ import { document } from 'global'; * i.e. ({ Component, data }). */ function getRenderedTree(story: any) { + // allow setContext to work + set_current_component({ $$: { context: new Map() } }); + const { Component, props } = story.render(); const DefaultCompatComponent = Component.default || Component; diff --git a/app/svelte/src/client/preview/PreviewRender.svelte b/app/svelte/src/client/preview/PreviewRender.svelte new file mode 100644 index 00000000000..40c027d6c80 --- /dev/null +++ b/app/svelte/src/client/preview/PreviewRender.svelte @@ -0,0 +1,37 @@ + + \ No newline at end of file diff --git a/app/svelte/src/client/preview/SlotDecorator.svelte b/app/svelte/src/client/preview/SlotDecorator.svelte new file mode 100644 index 00000000000..bd51e06f427 --- /dev/null +++ b/app/svelte/src/client/preview/SlotDecorator.svelte @@ -0,0 +1,30 @@ + +{#if decorator} + + + +{:else} + +{/if} \ No newline at end of file diff --git a/app/svelte/src/client/preview/decorators.ts b/app/svelte/src/client/preview/decorators.ts new file mode 100644 index 00000000000..1f9af116602 --- /dev/null +++ b/app/svelte/src/client/preview/decorators.ts @@ -0,0 +1,100 @@ +import { StoryFn, DecoratorFunction, StoryContext } from '@storybook/addons'; +import SlotDecorator from './SlotDecorator.svelte'; + +const defaultContext: StoryContext = { + id: 'unspecified', + name: 'unspecified', + kind: 'unspecified', + parameters: {}, + args: {}, + argTypes: {}, + globals: {}, +}; + +/** + * 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. + * @param obj object + */ +function unWrap(obj: any) { + return obj && obj.default ? obj.default : obj; +} + +/** + * Transform a story to be compatible with the PreviewRender component. + * + * - `() => MyComponent` is translated to `() => ({ Component: MyComponent })` + * - `() => ({})` is translated to `() => ({ Component: })` + * - A decorator component is wrapped with SlotDecorator. The decorated component is inject through + * a + * + * @param context StoryContext + * @param story the current story + * @param originalStory the story decorated by the current story + */ +function prepareStory(context: StoryContext, story: any, originalStory?: any) { + let result = unWrap(story); + if (isSvelteComponent(result)) { + // wrap the component + result = { + Component: result, + }; + } + + if (originalStory) { + // inject the new story as a wrapper of the original story + result = { + Component: SlotDecorator, + props: { + decorator: unWrap(result.Component), + decoratorProps: result.props, + component: unWrap(originalStory.Component), + props: originalStory.props, + on: originalStory.on, + }, + }; + } else { + let cpn = result.Component; + if (!cpn) { + // if the component is not defined, get it from parameters + cpn = context.parameters.component; + } + result.Component = unWrap(cpn); + } + return result; +} + +export function decorateStory(storyFn: any, decorators: any[]) { + return decorators.reduce( + (previousStoryFn: StoryFn, decorator: DecoratorFunction) => ( + context: StoryContext = defaultContext + ) => { + let story; + const decoratedStory = decorator( + ({ parameters, ...innerContext }: StoryContext = {} as StoryContext) => { + story = previousStoryFn({ ...context, ...innerContext }); + return story; + }, + context + ); + + if (!story) { + story = previousStoryFn(context); + } + + if (!decoratedStory || decoratedStory === story) { + return story; + } + + return prepareStory(context, decoratedStory, story); + }, + (context: StoryContext) => prepareStory(context, storyFn(context)) + ); +} diff --git a/app/svelte/src/client/preview/index.ts b/app/svelte/src/client/preview/index.ts index abc4d66ddb3..da734c6f7a4 100644 --- a/app/svelte/src/client/preview/index.ts +++ b/app/svelte/src/client/preview/index.ts @@ -1,9 +1,10 @@ import { start } from '@storybook/core/client'; +import { decorateStory } from './decorators'; import './globals'; import render from './render'; -const { configure: coreConfigure, clientApi, forceReRender } = start(render); +const { configure: coreConfigure, clientApi, forceReRender } = start(render, { decorateStory }); export const { setAddon, diff --git a/app/svelte/src/client/preview/render.ts b/app/svelte/src/client/preview/render.ts index cea239a2d15..ba072e1400a 100644 --- a/app/svelte/src/client/preview/render.ts +++ b/app/svelte/src/client/preview/render.ts @@ -1,7 +1,6 @@ -import { detach, insert, noop } from 'svelte/internal'; import { document } from 'global'; -import dedent from 'ts-dedent'; -import { MountViewArgs, RenderContext } from './types'; +import { RenderContext } from './types'; +import PreviewRender from './PreviewRender.svelte'; type Component = any; @@ -15,104 +14,21 @@ function cleanUpPreviousStory() { previousComponent = null; } -function createSlotFn(element: any) { - return [ - function createSlot() { - return { - c: noop, - m: function mount(target: any, anchor: any) { - insert(target, element, anchor); - }, - d: function destroy(detaching: boolean) { - if (detaching) { - detach(element); - } - }, - l: noop, - }; - }, - ]; -} - -function createSlots(slots: Record): Record { - return Object.entries(slots).reduce((acc, [slotName, element]) => { - acc[slotName] = createSlotFn(element); - return acc; - }, {} as Record); -} - -function mountView({ Component, target, props, on, Wrapper, WrapperData }: MountViewArgs) { - let component: Component; - - if (Wrapper) { - const fragment = document.createDocumentFragment(); - component = new Component({ target: fragment, props }); - - const wrapper = new Wrapper({ - target, - props: { - ...WrapperData, - $$slots: createSlots({ default: fragment }), - $$scope: {}, - }, - }); - component.$on('destroy', () => { - wrapper.$destroy(true); - }); - } else { - component = new Component({ target, props }); - } - - if (on) { - // Attach svelte event listeners. - Object.keys(on).forEach((eventName) => { - component.$on(eventName, on[eventName]); - }); - } - - previousComponent = component; -} - export default function render({ storyFn, kind, name, showMain, showError }: RenderContext) { - const { - /** @type {SvelteComponent} */ - Component, - /** @type {any} */ - props, - /** @type {{[string]: () => {}}} Attach svelte event handlers */ - on, - Wrapper, - WrapperData, - } = storyFn(); - cleanUpPreviousStory(); - const DefaultCompatComponent = Component ? Component.default || Component : undefined; - const DefaultCompatWrapper = Wrapper ? Wrapper.default || Wrapper : undefined; - - if (!DefaultCompatComponent) { - 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: {} })" - when defining the story. - `, - }); - - return; - } const target = document.getElementById('root'); target.innerHTML = ''; - mountView({ - Component: DefaultCompatComponent, + previousComponent = new PreviewRender({ target, - props, - on, - Wrapper: DefaultCompatWrapper, - WrapperData, + props: { + storyFn, + name, + kind, + showError, + }, }); showMain(); diff --git a/app/svelte/src/typings.d.ts b/app/svelte/src/typings.d.ts index 6288cba4b09..dcadca90cea 100644 --- a/app/svelte/src/typings.d.ts +++ b/app/svelte/src/typings.d.ts @@ -1,2 +1,3 @@ declare module '@storybook/core/*'; declare module 'global'; +declare module '*.svelte'; \ No newline at end of file diff --git a/docs/snippets/svelte/button-group-story.js.mdx b/docs/snippets/svelte/button-group-story.js.mdx index 1d031910bfd..8d5237a31a1 100644 --- a/docs/snippets/svelte/button-group-story.js.mdx +++ b/docs/snippets/svelte/button-group-story.js.mdx @@ -10,7 +10,6 @@ export default { } const Template = (args) => ({ - Component: ButtonGroup, props: args, }); diff --git a/docs/snippets/svelte/button-story-component-decorator.js.mdx b/docs/snippets/svelte/button-story-component-decorator.js.mdx index 9cda6ae6a1b..65dbf2bf638 100644 --- a/docs/snippets/svelte/button-story-component-decorator.js.mdx +++ b/docs/snippets/svelte/button-story-component-decorator.js.mdx @@ -7,17 +7,7 @@ import MarginDecorator from './MarginDecorator.svelte'; export default { title: "Button", component: Button, - decorators: [(storyFn) => { - const story = storyFun(); - - return { - Component: MarginDecorator, - props: { - child: story.Component, - props: story.props, - }, - } - }] + decorators: [() => MarginDecorator], }; // Your templates and stories here. @@ -28,20 +18,8 @@ export default { ```html - - - -
- +
diff --git a/examples/svelte-kitchen-sink/src/stories/__snapshots__/decorators.stories.storyshot b/examples/svelte-kitchen-sink/src/stories/__snapshots__/decorators.stories.storyshot new file mode 100644 index 00000000000..4ff491be39b --- /dev/null +++ b/examples/svelte-kitchen-sink/src/stories/__snapshots__/decorators.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Decorators Decorators 1`] = ` +
+
+
+
+ Value read from context: setted from decorator +
+ +
+ + + +
+ + +
+`; diff --git a/examples/svelte-kitchen-sink/src/stories/decorators.stories.js b/examples/svelte-kitchen-sink/src/stories/decorators.stories.js new file mode 100644 index 00000000000..6b9a1e6993c --- /dev/null +++ b/examples/svelte-kitchen-sink/src/stories/decorators.stories.js @@ -0,0 +1,24 @@ +import { setContext } from 'svelte'; +import { action } from '@storybook/addon-actions'; +import Context from '../components/Context.svelte'; +import BorderDecorator from './BorderDecorator.svelte'; + +export default { + title: 'Decorators', + component: Context, + decorators: [ + () => BorderDecorator, + () => ({ Component: BorderDecorator, props: { color: 'blue' } }), + ], +}; + +export const Decorators = () => ({ + on: { + click: action('I am logging in the actions tab'), + }, +}); +Decorators.decorators = [ + () => { + setContext('storybook/test', 'setted from decorator'); + }, +];