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

Addon-docs: Add dynamic snippet support for Svelte #13653

Merged
merged 1 commit into from
Jan 22, 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
3 changes: 3 additions & 0 deletions addons/docs/src/frameworks/svelte/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { extractArgTypes } from './extractArgTypes';
import { extractComponentDescription } from '../../lib/docgen';
import { prepareForInline } from './prepareForInline';
import { sourceDecorator } from './sourceDecorator';

export const parameters = {
docs: {
Expand All @@ -10,3 +11,5 @@ export const parameters = {
extractComponentDescription,
},
};

export const decorators = [sourceDecorator];
44 changes: 44 additions & 0 deletions addons/docs/src/frameworks/svelte/sourceDecorator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Args } from '@storybook/api';
import { generateSvelteSource } from './sourceDecorator';

expect.addSnapshotSerializer({
print: (val: any) => val,
test: (val) => typeof val === 'string',
});

function generateForArgs(args: Args, slotProperty: string = null) {
return generateSvelteSource({ name: 'Component' }, args, {}, slotProperty);
}

describe('generateSvelteSource', () => {
test('boolean true', () => {
expect(generateForArgs({ bool: true })).toMatchInlineSnapshot(`<Component bool/>`);
});
test('boolean false', () => {
expect(generateForArgs({ bool: false })).toMatchInlineSnapshot(`<Component bool={false}/>`);
});
test('null property', () => {
expect(generateForArgs({ propnull: null })).toMatchInlineSnapshot(`<Component />`);
});
test('string property', () => {
expect(generateForArgs({ str: 'mystr' })).toMatchInlineSnapshot(`<Component str="mystr"/>`);
});
test('number property', () => {
expect(generateForArgs({ count: 42 })).toMatchInlineSnapshot(`<Component count={42}/>`);
});
test('object property', () => {
expect(generateForArgs({ obj: { x: true } })).toMatchInlineSnapshot(
`<Component obj={{"x":true}}/>`
);
});
test('multiple properties', () => {
expect(generateForArgs({ a: 1, b: 2 })).toMatchInlineSnapshot(`<Component a={1} b={2}/>`);
});
test('slot property', () => {
expect(generateForArgs({ content: 'xyz', myProp: 'abc' }, 'content')).toMatchInlineSnapshot(`
<Component myProp="abc">
xyz
</Component>
`);
});
});
167 changes: 167 additions & 0 deletions addons/docs/src/frameworks/svelte/sourceDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { addons, StoryContext } from '@storybook/addons';
import { ArgTypes, Args } from '@storybook/api';

import { SourceType, SNIPPET_RENDERED } from '../../shared';

/**
* Check if the sourcecode should be generated.
*
* @param context StoryContext
*/
const skipSourceRender = (context: StoryContext) => {
const sourceParams = context?.parameters.docs?.source;
const isArgsStory = context?.parameters.__isArgsStory;

// always render if the user forces it
if (sourceParams?.type === SourceType.DYNAMIC) {
return false;
}

// never render if the user is forcing the block to render code, or
// if the user provides code, or if it's not an args story.
return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE;
};

/**
* Transform a key/value to a svelte declaration as string.
*
* Default values are ommited
*
* @param key Key
* @param value Value
* @param argTypes Component ArgTypes
*/
function toSvelteProperty(key: string, value: any, argTypes: ArgTypes): string {
if (value === undefined || value === null) {
return null;
}

// default value ?
if (argTypes[key] && argTypes[key].defaultValue === value) {
return null;
}

if (value === true) {
return key;
}

if (typeof value === 'string') {
return `${key}=${JSON.stringify(value)}`;
}

return `${key}={${JSON.stringify(value)}}`;
}

/**
* Extract a component name.
*
* @param component Component
*/
function getComponentName(component: any): string {
const { __docgen = {} } = component;
let { name } = __docgen;

if (!name) {
return component.name;
}

if (name.endsWith('.svelte')) {
name = name.substring(0, name.length - 7);
}
return name;
}

/**
* Generate a svelte template.
*
* @param component Component
* @param args Args
* @param argTypes ArgTypes
* @param slotProperty Property used to simulate a slot
*/
export function generateSvelteSource(
component: any,
args: Args,
argTypes: ArgTypes,
slotProperty: string
): string {
const name = getComponentName(component);

if (!name) {
return null;
}

const props = Object.entries(args)
.filter(([k]) => k !== slotProperty)
.map(([k, v]) => toSvelteProperty(k, v, argTypes))
.filter((p) => p)
.join(' ');

const slotValue = slotProperty ? args[slotProperty] : null;

if (slotValue) {
return `<${name} ${props}>\n ${slotValue}\n</${name}>`;
}

return `<${name} ${props}/>`;
}

/**
* Check if the story component is a wrapper to the real component.
*
* A component can be annoted with @wrapper to indicate that
* it's just a wrapper for the real tested component. If it's the case
* then the code generated references the real component, not the wrapper.
*
* moreover, a wrapper can annotate a property with @slot : this property
* is then assumed to be an alias to the default slot.
*
* @param component Component
*/
function getWrapperProperties(component: any) {
const { __docgen } = component;
if (!__docgen) {
return { wrapper: false };
}

// the component should be declared as a wrapper
if (!__docgen.keywords.find((kw: any) => kw.name === 'wrapper')) {
return { wrapper: false };
}

const slotProp = __docgen.data.find((prop: any) =>
prop.keywords.find((kw: any) => kw.name === 'slot')
);
return { wrapper: true, slotProperty: slotProp?.name as string };
}

/**
* Svelte source decorator.
* @param storyFn Fn
* @param context StoryContext
*/
export const sourceDecorator = (storyFn: any, context: StoryContext) => {
const story = storyFn();

if (skipSourceRender(context)) {
return story;
}

const channel = addons.getChannel();

const { parameters = {}, args = {} } = context || {};
let { Component: component = {} } = story;

const { wrapper, slotProperty } = getWrapperProperties(component);
if (wrapper) {
component = parameters.component;
}

const source = generateSvelteSource(component, args, context?.argTypes, slotProperty);

if (source) {
channel.emit(SNIPPET_RENDERED, (context || {}).id, source);
}

return story;
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ exports[`Storyshots Button Rounded 1`] = `



You clicked
Rounded
:
0
</button>
Expand Down Expand Up @@ -63,7 +63,7 @@ exports[`Storyshots Button Square 1`] = `



You clicked
Square
:
0
</button>
Expand Down
7 changes: 4 additions & 3 deletions examples/svelte-kitchen-sink/src/stories/button.stories.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import ButtonView from './views/ButtonView.svelte';
import Button from '../components/Button.svelte';

export default {
title: 'Button',
component: ButtonView,
component: Button,
};

const Template = (args) => ({
Expand All @@ -15,11 +16,11 @@ const Template = (args) => ({
export const Rounded = Template.bind({});
Rounded.args = {
rounded: true,
message: 'Squared text',
text: 'Rounded',
};

export const Square = Template.bind({});
Square.args = {
rounded: false,
message: 'Squared text',
text: 'Square',
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script>
import Button from '../../components/Button.svelte';

/**
* @component Button View
* @wrapper
*/
import Button from '../../components/Button.svelte';


/**
* Rounds the button
Expand All @@ -17,6 +18,7 @@

/**
* Button text
* @slot
*/
export let text = 'You clicked';

Expand Down