Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svelte: Improved decorators #13785

Merged
merged 1 commit into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions addons/docs/src/frameworks/svelte/HOC.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
export let component;
export let props;
export let storyFn;

let { Component: component, props } = storyFn();
</script>

<svelte:options accessors={true} />
<svelte:component this={component} {...props}/>
11 changes: 3 additions & 8 deletions addons/docs/src/frameworks/svelte/prepareForInline.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
3 changes: 2 additions & 1 deletion addons/storyshots/storyshots-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
"react-dom": "^16.8.0 || ^17.0.0",
"rxjs": "*",
"vue": "*",
"vue-jest": "*"
"vue-jest": "*",
"svelte": "*"
},
"peerDependenciesMeta": {
"@storybook/angular": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions app/svelte/src/client/preview/PreviewRender.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script>
import SlotDecorator from './SlotDecorator.svelte';
import dedent from 'ts-dedent';

export let name;
export let kind;
export let storyFn;
export let showError;

const {
/** @type {SvelteComponent} */
Component,
/** @type {any} */
props = {},
/** @type {{[string]: () => {}}} Attach svelte event handlers */
on,
Wrapper,
WrapperData = {},
} = storyFn();

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: {} })"
when defining the story.
`,
});
}
</script>
<SlotDecorator
decorator={Wrapper}
decoratorProps={WrapperData}
component={Component}
props={props}
{on}/>
30 changes: 30 additions & 0 deletions app/svelte/src/client/preview/SlotDecorator.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script>
import { onMount } from 'svelte';
export let decorator;
export let decoratorProps = {};
export let component;
export let props = {};
export let on;

let instance;
let decoratorInstance;

function getInstance() {
// instance can be undefined if a decorator doesn't have <slot/>
return instance || decoratorInstance;
}

if (on) {
// Attach svelte event listeners.
Object.keys(on).forEach((eventName) => {
onMount(() => getInstance().$on(eventName, on[eventName]));
});
}
</script>
{#if decorator}
<svelte:component this={decorator} {...decoratorProps} bind:this={decoratorInstance}>
<svelte:component this={component} {...props} bind:this={instance}/>
</svelte:component>
{:else}
<svelte:component this={component} {...props} bind:this={instance}/>
{/if}
100 changes: 100 additions & 0 deletions app/svelte/src/client/preview/decorators.ts
Original file line number Diff line number Diff line change
@@ -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: <from parameters.component> })`
* - A decorator component is wrapped with SlotDecorator. The decorated component is inject through
* a <slot/>
*
* @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))
);
}
3 changes: 2 additions & 1 deletion app/svelte/src/client/preview/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
102 changes: 9 additions & 93 deletions app/svelte/src/client/preview/render.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<string, any>): Record<string, any> {
return Object.entries(slots).reduce((acc, [slotName, element]) => {
acc[slotName] = createSlotFn(element);
return acc;
}, {} as Record<string, any>);
}

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();
Expand Down
1 change: 1 addition & 0 deletions app/svelte/src/typings.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
declare module '@storybook/core/*';
declare module 'global';
declare module '*.svelte';
1 change: 0 additions & 1 deletion docs/snippets/svelte/button-group-story.js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default {
}

const Template = (args) => ({
Component: ButtonGroup,
props: args,
});

Expand Down
Loading