Skip to content

Commit

Permalink
Merge pull request storybookjs#15337 from bennypowers/patch-1
Browse files Browse the repository at this point in the history
Web-components: Dynamic source snippets
  • Loading branch information
shilman authored Jul 10, 2021
2 parents f6c296c + 9a5dccd commit 31e3ad8
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 172 deletions.
29 changes: 0 additions & 29 deletions addons/docs/src/frameworks/web-components/config.js

This file was deleted.

19 changes: 19 additions & 0 deletions addons/docs/src/frameworks/web-components/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { extractArgTypes, extractComponentDescription } from './custom-elements';
import { sourceDecorator } from './sourceDecorator';
import { prepareForInline } from './prepareForInline';
import { SourceType } from '../../shared';

export const decorators = [sourceDecorator];

export const parameters = {
docs: {
extractArgTypes,
extractComponentDescription,
inlineStories: true,
prepareForInline,
source: {
type: SourceType.DYNAMIC,
language: 'html',
},
},
};
19 changes: 19 additions & 0 deletions addons/docs/src/frameworks/web-components/prepareForInline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { StoryFn } from '@storybook/addons';
import React from 'react';
import { render } from 'lit-html';

export const prepareForInline = (storyFn: StoryFn) => {
class Story extends React.Component {
wrapperRef = React.createRef<HTMLElement>();

componentDidMount(): void {
render(storyFn(), this.wrapperRef.current);
}

render(): React.ReactElement {
return React.createElement('div', { ref: this.wrapperRef });
}
}

return (React.createElement(Story) as unknown) as React.CElement<{}, React.Component>;
};
100 changes: 100 additions & 0 deletions addons/docs/src/frameworks/web-components/sourceDecorator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { html } from 'lit-html';
import { styleMap } from 'lit-html/directives/style-map';
import { addons, StoryContext } from '@storybook/addons';
import { sourceDecorator } from './sourceDecorator';
import { SNIPPET_RENDERED } from '../../shared';

jest.mock('@storybook/addons');
const mockedAddons = addons as jest.Mocked<typeof addons>;

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

const makeContext = (name: string, parameters: any, args: any, extra?: object): StoryContext => ({
id: `lit-test--${name}`,
kind: 'js-text',
name,
parameters,
args,
argTypes: {},
globals: {},
...extra,
});

describe('sourceDecorator', () => {
let mockChannel: { on: jest.Mock; emit?: jest.Mock };
beforeEach(() => {
mockedAddons.getChannel.mockReset();

mockChannel = { on: jest.fn(), emit: jest.fn() };
mockedAddons.getChannel.mockReturnValue(mockChannel as any);
});

it('should render dynamically for args stories', () => {
const storyFn = (args: any) => html`<div>args story</div>`;
const context = makeContext('args', { __isArgsStory: true }, {});
sourceDecorator(storyFn, context);
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'lit-test--args',
'<div>args story</div>'
);
});

it('should skip dynamic rendering for no-args stories', () => {
const storyFn = () => html`<div>classic story</div>`;
const context = makeContext('classic', {}, {});
sourceDecorator(storyFn, context);
expect(mockChannel.emit).not.toHaveBeenCalled();
});

it('should use the originalStoryFn if excludeDecorators is set', () => {
const storyFn = (args: any) => html`<div>args story</div>`;
const decoratedStoryFn = (args: any) => html`
<div style=${styleMap({ padding: `${25}px`, border: '3px solid red' })}>${storyFn(args)}</div>
`;
const context = makeContext(
'args',
{
__isArgsStory: true,
docs: {
source: {
excludeDecorators: true,
},
},
},
{},
{ originalStoryFn: storyFn }
);
sourceDecorator(decoratedStoryFn, context);
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'lit-test--args',
'<div>args story</div>'
);
});

it('allows the snippet output to be modified by transformSource', () => {
const storyFn = (args: any) => html`<div>args story</div>`;
const transformSource = (dom: string) => `<p>${dom}</p>`;
const docs = { transformSource };
const context = makeContext('args', { __isArgsStory: true, docs }, {});
sourceDecorator(storyFn, context);
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'lit-test--args',
'<p><div>args story</div></p>'
);
});

it('provides the story context to transformSource', () => {
const storyFn = (args: any) => html`<div>args story</div>`;
const transformSource = jest.fn((x) => x);
const docs = { transformSource };
const context = makeContext('args', { __isArgsStory: true, docs }, {});
sourceDecorator(storyFn, context);
expect(transformSource).toHaveBeenCalledWith('<div>args story</div>', context);
});
});
39 changes: 39 additions & 0 deletions addons/docs/src/frameworks/web-components/sourceDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* global window */
import { render } from 'lit-html';
import { addons, StoryContext, StoryFn } from '@storybook/addons';
import { SNIPPET_RENDERED, SourceType } from '../../shared';

function 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;
}

function applyTransformSource(source: string, context: StoryContext): string {
const { transformSource } = context.parameters.docs ?? {};
if (typeof transformSource !== 'function') return source;
return transformSource(source, context);
}

export function sourceDecorator(storyFn: StoryFn, context: StoryContext) {
const story = context?.parameters.docs?.source?.excludeDecorators
? context.originalStoryFn(context.args)
: storyFn();

if (!skipSourceRender(context)) {
const container = window.document.createElement('div');
render(story, container);
const source = applyTransformSource(container.innerHTML.replace(/<!---->/g, ''), context);
if (source) addons.getChannel().emit(SNIPPET_RENDERED, context.id, source);
}

return story;
}
2 changes: 1 addition & 1 deletion docs/frameworks.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ module.exports = {
},
{
name: 'Dynamic source',
supported: ['react', 'vue', 'angular', 'svelte'],
supported: ['react', 'vue', 'angular', 'svelte', 'web-components'],
path: 'writing-docs/doc-blocks#source',
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';

import { SbButton } from './sb-button';

Expand All @@ -20,9 +21,9 @@ export default {
const Template: Story<SbButton> = ({ primary, backgroundColor, size, label }) =>
html`<sb-button
?primary="${primary}"
.size="${size}"
.label="${label}"
.backgroundColor="${backgroundColor}"
size="${ifDefined(size)}"
label="${ifDefined(label)}"
background-color="${ifDefined(backgroundColor)}"
></sb-button>`;

export const Primary: Story<SbButton> = Template.bind({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class SbButton extends LitElement {
label: { type: String, reflect: true },
primary: { type: Boolean },
size: { type: String },
backgroundColor: { type: String },
backgroundColor: { type: String, attribute: 'background-color' },
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { html } from 'lit-html';

<Meta
title="Addons / Docs / Dynamic Source"
component="sb-button"
argTypes={{
size: { type: 'select', options: ['large', 'small'] },
label: { type: 'string' },
primary: { type: 'boolean' },
backgroundColor: { type: 'color', presetColors: ['white', 'transparent', 'blue'] },
}}
/>

# Dynamic Source

Stories can use Dynamic Source to display the result of changes to controls.

<Canvas withSource="open">
<Story
name="Button"
component="sb-button"
args={{
size: 'large',
label: 'Button',
primary: false,
backgroundColor: undefined,
}}
>
{(args) => html`
<sb-button
?primary="${args.primary}"
.size="${args.size}"
.label="${args.label}"
.backgroundColor="${args.backgroundColor}"
>
</sb-button>
`}
</Story>
</Canvas>

<ArgsTable story="Button" />
Loading

0 comments on commit 31e3ad8

Please sign in to comment.