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: Svelte syntax Component Story Format #7682

Closed
wants to merge 19 commits into from
Closed
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
10 changes: 0 additions & 10 deletions addons/centered/src/components/Centered.svelte

This file was deleted.

46 changes: 21 additions & 25 deletions addons/centered/src/svelte.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,32 @@
/* global document */

import { makeDecorator } from '@storybook/addons';
import Centered from './components/Centered.svelte';
import styles from './styles';
import json2CSS from './helpers/json2CSS';
import parameters from './parameters';

const centeredStyles = {
/** @type {string} */
style: json2CSS(styles.style),
/** @type {string} */
innerStyle: json2CSS(styles.innerStyle),
const createStyled = (style: Partial<CSSStyleDeclaration>) => () => {
const element = document.createElement('div') as HTMLDivElement;
Object.assign(element.style, style);
return element;
};

/**
* This functionality works by passing the svelte story component into another
* svelte component that has the single purpose of centering the story component
* using a wrapper and container.
*
* We use the special element <svelte:component /> to achieve this.
*
* @see https://svelte.technology/guide#svelte-component
*/
function centered(storyFn: () => any) {
const { Component: OriginalComponent, props, on } = storyFn();
const createInner = createStyled(styles.innerStyle);

const createOuter = createStyled(styles.style);

return {
Component: OriginalComponent,
props,
on,
Wrapper: Centered,
WrapperData: centeredStyles,
// domWrappers allows to decorate the mounting point with direct DOM manipulation,
// without the need to provide a specific Svelte component
const centered = (storyFn: () => any) => {
const { domWrappers = [], ...story } = storyFn();
const centeredDomWrapper = (target: HTMLElement) => {
const outer = createOuter();
const inner = createInner();
target.appendChild(outer);
outer.appendChild(inner);
return inner;
};
}
return { ...story, domWrappers: [...domWrappers, centeredDomWrapper] };
};

export default makeDecorator({
...parameters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ import { document } from 'global';
* i.e. ({ Component, data }).
*/
function getRenderedTree(story) {
const { Component, props } = story.render();

const DefaultCompatComponent = Component.default || Component;
const result = story.render();

// We need to create a target to mount onto.
const target = document.createElement('section');

new DefaultCompatComponent({ target, props }); // eslint-disable-line
if (typeof result === 'function') {
// svelte format stories (.stories.svelte)
const Component = result;
new Component({ target }); // eslint-disable-line
} else {
const { Component, props } = result;
const DefaultCompatComponent = Component.default || Component;
new DefaultCompatComponent({ target, props }); // eslint-disable-line
}

// Classify the target so that it is clear where the markup
// originates from, and that it is specific for snapshot tests.
Expand Down
1 change: 1 addition & 0 deletions app/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/client-logger": "5.2.0-beta.37",
"@storybook/core": "5.2.0-beta.37",
"common-tags": "^1.8.0",
"core-js": "^3.0.1",
Expand Down
24 changes: 24 additions & 0 deletions app/svelte/src/client/components/Meta.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script>
import { getRegister, getRender } from './context';

export let title;
export let decorators;
export let parameters;

const register = getRegister();
const render = getRender();

const isMyKind = ({ selectedKind }) => selectedKind === title;

if (register) {
register.addMeta({
title,
decorators,
parameters,
});
}
</script>

{#if register || render && isMyKind(render)}
<slot />
{/if}
14 changes: 14 additions & 0 deletions app/svelte/src/client/components/Preview.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
import { setRender, setRegister } from './context';

export let Stories;
export let selectedKind;
export let selectedStory;

const render = { selectedKind, selectedStory };

setRegister(null);
setRender(render);
</script>

<svelte:component this={Stories} />
23 changes: 23 additions & 0 deletions app/svelte/src/client/components/PreviewRenderer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--
This component is the entry point for the `render` method.

It passes the story (i.e. Component & props) and the initial wrappers to
PreviewWrapper, that will render itself recursively until it has consumed all
wrappers in the pipeline, and eventually render the story's Component.
-->

<script>
import { stripIndents } from 'common-tags';

import PreviewWrapper from './PreviewWrapper.svelte';

export let story;

if (!story) {
throw new Error('Missing story');
}

const { wrappers = [] } = story;
</script>

<PreviewWrapper {story} {wrappers} />
41 changes: 41 additions & 0 deletions app/svelte/src/client/components/PreviewWrapper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script>
export let story;
export let wrappers;

// --- wrappers ---

const resolveWrapper = wrapper => {
if (!wrapper) {
return null;
} else if (typeof wrapper === 'function') {
return { Component: wrapper };
} else if (wrapper.Component) {
return wrapper;
} else {
throw new Error('Unsupported wrapper format');
}
};

const remainingWrappers = [...wrappers];
const nextWrapper = resolveWrapper(remainingWrappers.pop())

// --- Component ---

let cmp

$: if (cmp && story.on) {
const { on: eventHandlers } = story;
Object.keys(eventHandlers).forEach(event => {
const handler = eventHandlers[event];
cmp.$on(event, handler);
});
}
</script>

{#if nextWrapper}
<svelte:component this={nextWrapper.Component} {...nextWrapper.props}>
<svelte:self {story} wrappers={remainingWrappers} />
</svelte:component>
{:else}
<svelte:component this={story.Component} {...story.props} bind:this={cmp} />
{/if}
12 changes: 12 additions & 0 deletions app/svelte/src/client/components/RegisterContext.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import { setContext } from 'svelte';
import { setRender, setRegister } from './context';

export let Stories;
export let register;

setRender(null);
setRegister(register);
</script>

<svelte:component this={Stories} />
27 changes: 27 additions & 0 deletions app/svelte/src/client/components/Story.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script>
import { getRegister, getRender } from './context';

export let name;
export let parameters;
export let decorators; // TODO

if (!name) {
throw new Error('Missing Story name');
}

const render = getRender();
const renderThis = render && render.selectedStory === name;

const register = getRegister();
if (register) {
register.addStory({
name,
decorators,
parameters,
});
}
</script>

{#if renderThis}
<slot />
{/if}
12 changes: 12 additions & 0 deletions app/svelte/src/client/components/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getContext, setContext } from 'svelte';

const REGISTER = '__STORYBOOK_register';
const RENDER = '__STORYBOOK_render';

export const setRegister = value => setContext(REGISTER, value);

export const getRegister = () => getContext(REGISTER);

export const setRender = value => setContext(RENDER, value);

export const getRender = () => getContext(RENDER);
86 changes: 86 additions & 0 deletions app/svelte/src/client/extract-csf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-env browser */

import RegisterContext from './components/RegisterContext.svelte';
import Preview from './components/Preview.svelte';

// This is the name that will be used for the story named 'default'.
//
// If the `_default` export is already taken (by a user's own named export), then
// a numeric index will be appended and increased until a free name is found.
//
// TODO What's the officially recommended default name?
//
const canonicalDefaultName = '_default';

const createFragment = document.createDocumentFragment
? () => document.createDocumentFragment()
: () => document.createElement('div');

const defaultStoryName = xports => {
if (!xports[canonicalDefaultName]) {
return canonicalDefaultName;
}
let lastDefaultIndex = 0;
let name;
do {
lastDefaultIndex += 1;
name = `${canonicalDefaultName}${lastDefaultIndex}`;
} while (xports[name] !== undefined);
return name;
};

// Extracts CSF from a Svelte component's `module.exports` object. The returned
// value is expected to be used to replace the component's `module.exports` in
// order to turn the component module into a propper CSF module.
export default xports => {
const Stories = xports.default;

const result = {};

const register = {
addMeta: config => {
if (result.default) {
throw new Error('Only one meta component is allowed per stories file');
}
result.default = config;
},
addStory: story => {
const { name } = story;
const storyFn = () => ({
Component: Preview,
props: {
Stories,
selectedKind: result.default.title,
selectedStory: name,
},
});
storyFn.story = story;
const prop = name === 'default' ? defaultStoryName(xports) : name;
result[prop] = storyFn;
},
};

// run a register phase to render Story and Meta components that will
// register themselves
const cmp = new RegisterContext({
target: createFragment(),
props: {
Stories,
register,
},
});
cmp.$destroy();

// goal: not having an error while a stories file is still empty (when it has
// just been created)
if (!result.default) {
return xports;
}
if (!result.default.title) {
throw new Error('Meta component with title is required');
}

result.default.excludeStories = Object.keys(xports).filter(name => name !== 'default');

return Object.assign({}, xports, result);
};
3 changes: 3 additions & 0 deletions app/svelte/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export {
raw,
} from './preview';

export { default as Meta } from './components/Meta.svelte';
export { default as Story } from './components/Story.svelte';

if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
Loading