-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Web-components: Dynamic source snippets #15337
Merged
Merged
Changes from 11 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
49f8c41
feat(web-components): dynamic source
bennypowers 64efb26
test(web-components): dynamic stories
bennypowers fbacf4e
style(web-components): refactor to TS
bennypowers e584062
fix(web-components): types
bennypowers b66e3b2
docs(web-components): add web-components to Dynamic Source support table
bennypowers d1039bc
docs(web-components): kitchen sink dynamic source example
bennypowers 3fd64f9
Merge branch 'next' into pr/15337
shilman 65e7803
Fix jest with lit-html ESM
shilman e395fff
fix(addons): webcomponents transformSource
bennypowers e2d34ea
style(addons): remove null check
bennypowers 2c47a6e
docs(web-components): set attributes in example story
bennypowers 9a5dccd
fix(addons): webcomponents excludeDecorators
bennypowers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
19
addons/docs/src/frameworks/web-components/prepareForInline.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
}; |
73 changes: 73 additions & 0 deletions
73
addons/docs/src/frameworks/web-components/sourceDecorator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { html } from 'lit-html'; | ||
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('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); | ||
}); | ||
}); |
37 changes: 37 additions & 0 deletions
37
addons/docs/src/frameworks/web-components/sourceDecorator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* 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 = storyFn(); | ||
|
||
if (!skipSourceRender(context)) { | ||
const container = window.document.createElement('div'); | ||
render(story, container); | ||
const source = applyTransformSource(container.innerHTML.replace(/<!---->/g, ''), context); | ||
brion-fuller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (source) addons.getChannel().emit(SNIPPET_RENDERED, context.id, source); | ||
} | ||
|
||
return story; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
.../web-components-kitchen-sink/src/stories/addons/docs/dynamic-source.stories.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" /> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do others feel about
context.originalStoryFn
here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are my thoughts on the matter: #14652 (comment)
Since this is all brand new in web-components, we can make it opt-out OR opt-in, depending on how you guys feel. I believe it's safer to include the decorators by default and allow users to opt-out of the decorators. In the React world, it's quite common for stories to rely on the decorators to provide context to render properly. But I'll defer to your collective judgment for what's best in the web-components world.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Westbrook can you elaborate on the case, both ways?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I’m less worried as long as it’s configurable. However, for my library I use a very Storybook custom decorator to control theme color/scale/direction in a way that you wouldn’t want to require a copy/paster to clean up after the fact.
storyFn()
leaves all of the decorator code whereas without it you copy just the component under demo.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bennypowers @Westbrook WDYT about adding configuration in this PR? i'm afraid if it doesn't get done now it will slip through the cracks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to put this in i'm just unfamiliar with the relevant apis.
is it just
??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make it consistent with React:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added with unit tests, thanks