Skip to content

Commit

Permalink
Svelte: Implements dynamic snippet
Browse files Browse the repository at this point in the history
  • Loading branch information
j3rem1e committed Jan 20, 2021
1 parent afd0eb5 commit 4e18a8f
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 7 deletions.
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

0 comments on commit 4e18a8f

Please sign in to comment.