These helpers are designed to make integrating Web Components with Storybook easier.
There are a number of things that this helper library does to provide developers a better experience with Storybook and Web Components:
- Uses types to provide better controls
- Prevents name collisions when attributes, properties, slots, and CSS shadow parts share the same name
- Provides a template with bindings for attributes, properties, CSS custom properties, and CSS shadow parts.
- Provides two-way binding for controls and attributes in the template to help keep control values in sync with the component
- Follow the installation steps in the Storybook docs
- Load you custom elements manifest into Storybook in the
preview.js
file:
import { setCustomElementsManifest } from "@storybook/web-components";
import customElements from "./path/to/custom-elements.json";
setCustomElementsManifest(customElements);
- Add the expanded controls to your config in the
preview.js
file:
export const parameters = {
...
controls: {
expanded: true,
...
},
}
npm i -D wc-storybook-helpers
Import the storybook helpers into your story:
import { getWcStorybookHelpers } from "wc-storybook-helpers";
Pass your element's tag name into the Storybook helper function.
const { events, args, argTypes, template } =
getWcStorybookHelpers("my-element");
Add the argTypes
and events
to your story config:
NOTE: If you are using using Storybook v6 the default values are included as part of the
argTypes
. If you are using v7 you will need to include theargs
object from the helpers and add them to the default export.
// Storybook v6
export default {
title: "Components/My Element",
component: "my-element",
argTypes,
parameters: {
actions: {
handles: events,
},
},
};
// Storybook v7
import type { Meta, StoryObj } from "@storybook/web-components";
const meta: Meta<MyElement> = {
title: "Components/My Element",
component: "my-element",
args, // <- default values for Storybook v7
argTypes,
parameters: {
actions: {
handles: events,
},
},
};
export default meta;
Add the template to your story's template and pass in the story args
into the template
function (this is an optional parameter, but required for arguments to function properly):
// Storybook v6
const DefaultTemplate = (args: any) => template(args);
export const Default: any = DefaultTemplate.bind({});
Default.args = {};
// Storybook v7
/**
* create Story type that will provide autocomplete and docs for `args`,
* but also allow for namespaced args like CSS Shadow Parts and Slots
*/
type Story = StoryObj<MyElement & typeof args>;
export const Default: Story = {
render: (args) => template(args),
args: {},
};
Based on the data in the custom elements manifest, the helpers will apply appropriate descriptions and control types to your arguments.
The default control types are not always the most helpful. The helper will use your types try to identify the appropriate input and options for your control.
For example if your component has an attribute called variant
with predefined values, the helper will convert it to a select that is pre-populated with the appropriate values and the default value selected.
One of the challenges with the default implementation is that if there are multiple properties with the same name, they will be overridden. For example, if there is an attribute named label
as well as a slot named label
only one will display. In order to ensure every argument is displayed properly, CSS Shadow Part and Slot arguments will be suffixed with -part
, and -slot
respectively. CSS Custom Properties don't receive one because they already have a unique property value and attributes and properties will rely on the camel-cased property name.
The reference name will be documented with the control's description.
That reference can then be used to bind default values to the template.
const DefaultTemplate = (args: any) => template(args);
export const Default: any = DefaultTemplate.bind({});
Default.args = {
docsHint: "Some other value than the default",
};
// Storybook v7
export const Default: Story = {
render: (args) => template(args),
args: {
docsHint: "Some other value than the default",
},
};
If you use the @deprecated
tag in your jsDoc descriptions, those will also display in the description.
/**
* @deprecated replaced by `docs-hint`
* Copy for the read the docs hint.
*/
@property({ attribute: "old-docs-hint", reflect: true })
oldDocsHint = "Click on the Vite and Lit logos to learn more";
If you would like to change any of your controls, you can easily override it using the spread operator and passing in an updated argType
after the helper argTypes
.
export default {
title: "Components/My Element",
component: "my-element",
argTypes: {
...argTypes,
docsHintAttr: {
name: 'docs-hint',
description: '...',
defaultValue: '...',
control: {
type: '...',
},
table: {
category: 'attributes',
defaultValue: {
summary: '...',
},
type: {
summary: '`string`',
},
},
},
...
};
If you want to capture the events output by your component, you can map them to your story's config under the parameter's section.
Note: They will only be captured if the bubbles
option on your CustomEvent
is set to true
(note - it is true
by default).
export default {
...
parameters: {
actions: {
handles: events,
},
},
};
If you would like to map additional events to your story, you can use the spread operator to extend the values.
export default {
...
parameters: {
actions: {
handles: [...events, 'my-other-event'],
},
},
};
If you are migrating from v6 to v7, and important note is that there were a number of APIs removed from the default project including the ability to automatically capture events in the Actions
tab. To add it back in, you will need to update your stories with the withActions
decorator.
import { withActions } from '@storybook/addon-actions/decorator';
const { args, argTypes, events, template } = getWcStorybookHelpers('my-element');
const meta: Meta<MyElement> = {
...
parameters: {
actions: {
handles: events,
},
},
decorators: [withActions],
};
export default meta;
Templates are configured to automatically map the control's attributes, properties, CSS custom properties, and CSS shadow parts to your element as well as provide two-way data binding for the component attributes back to the controls to keep them in sync.
Templates take 2 arguments - story arguments and slot data. You can use the controls and story args
to provide slot data, but if you want more granular control, using the slot
parameter on the template with more editor support.
const SelectTemplate = (args: any) =>
template(
args,
html`
<span slot="label">My Select</span>
<my-option>Option 1</my-option>
<my-option>Option 2</my-option>
<my-option>Option 3</my-option>
`
);
export const Default: any = SelectTemplate.bind({});
Default.args = {
docsHint: "Some other value than the default",
};
Component templates can be interpolated into a story's template with additional content.
const FormTemplate = (args: any) => html`
<form>
${template(
args,
html`
<span slot="label">My Select</span>
<my-option>Option 1</my-option>
<my-option>Option 2</my-option>
<my-option>Option 3</my-option>
`
)}
<button>Submit</button>
</form>
`;
The template also exposes a variable named component
that references the custom element so you can use custom logic with it.
const ComponentTemplate = (args: any) => html`
${template(
args,
html`
<span slot="label">My Select</span>
<my-option value="1">Option 1</my-option>
<my-option value="2">Option 2</my-option>
<my-option value="3">Option 3</my-option>
`
)}
<script>
// set property values
component.value = "2";
// call component methods
component.show();
</script>
`;
If you are using the template
, using slots form the controls panel is fairly straight forward. The input is already wired up to the appropriate slot and so rich content can be added directly to the input with no additional set-up required.
Like the slot controls, the template
makes working with CSS Shadow Parts easy. The template is pre-configured with the appropriate code to apply styles to the component's parts. You can simply apply the styles directly to the control input.
The helpers package provides a way to set global configurations for your stories using the setWcStorybookHelpersConfig
function. This can be added to the .storybook/preview.js
file.
//preview.js
import { setWcStorybookHelpersConfig } from "wc-storybook-helpers";
setWcStorybookHelpersConfig({ ... });
setCustomElementsManifest(customElements);
The helpers can be passed the following options:
interface Options {
/** hides the `arg ref` label on each control */
hideArgRef?: boolean;
/** sets the custom type reference in the Custom Elements Manifest */
typeRef?: string;
/** hides the <script> tag, doesn't render it in the story/component source code */
hideScriptTag?: boolean;
/** doesn't render attributes when their value is equal to the default value of that attribute */
renderDefaultAttributeValues?: boolean;
}
There may be times you want to hide the "arg ref" label. You can set the hideArgRef
to true
and it will remove the label from controls.
setWcStorybookHelpersConfig({ hideArgRef: true });
It is common for teams to parse or create custom types and add them to the Custom Elements Manifest to use for other tools (if you're not already, CEM Analyzer Expanded Types plugin) can help with this. The helpers can be configured to use those types instead of the default types in your manifest using the typeRef
. If no custom type is found, it will fallback to the default type.
setWcStorybookHelpersConfig({ typeRef: "expandedType" });
Every story using the template
helper includes a script tag with a reference to the custom element in the component
variable. The hideScriptTag
option removes this script tag and the variable.
setWcStorybookHelpersConfig({ hideScriptTag: true });
If an arg
value matches the default value, it will not be added to the component. To always show the default values, enable the renderDefaultAttributeValues
setting:
setWcStorybookHelpersConfig({ renderDefaultAttributeValues: true });