No Preview

Sorry, but you either have no stories or none are selected somehow.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

Writing Stories

Writing stories for Storybook using the Component Story Format (CSF)

Official Docs

What is a story?

Stories the are building blocks of Storybook, they represent a snapshot or slice of the component at a particular state, rendered onto a "playground" or "sandbox" for anyone to interact with. In turn, we can take these stories and embed them into MDX files (like the one you're reading right now) and turn them into living documentation.

For example, the WithSuccess story from input.stories.js gets rendered as:

This is a success message

# input.stories.js
import DtInput from './input';

...

// Template of the story component, this is a harness Vue component that
// wraps the DtInput component
const Template = (args, { argTypes }) => {
  return {
    components: { DtInput },
    template: baseInputTemplate,
    props: Object.keys(argTypes),
    methods: { onInput: action('input') },
  };
};

...

// Snapshotting props
export const WithSuccess = Template.bind({});
WithSuccess.args = {
  messages: [{ message: 'This is a success message', type: 'success' }],
};

We can interact with this component as we would in our actual application. By modifying the args property, we can specify any combination of states for this component.

Component Story Format and Export Structure

Storybook automatically parses the default and named exports from every *.stories.js file to generate stories, this is known as the Component Story Format

Default Export

Default export is where the metadata and configuration for all of the stories contained within the same *.stories.js file are defined. It also provides fields for addon configuration for said stories.

// input.stories.js

import DtInput from './input';

export default {
  title: 'Forms/Input',
  component: DtInput,
  parameters: { ... },
  decorators: [ ... ],
};

Where parameters holds the configuration for addons and decorators are templates that wrap our stories.

Named Exports

Named exports are the individual stories that show up in the sidebar of storybooks

When writing stories, it's good practice to create a Template variable that is not exported which binds all of the props, events, and data that we want for all of our stories. It acts as the harness for which our components are held. The Template is reused in each story as named exports and we can simply specify the args field to set props.

// input.stories.js

...

const Template = (args, { argTypes }) => {
  return {
    components: { DtInput },
    template: baseInputTemplate,
    props: Object.keys(argTypes),
  };
};

// Dt Input -> Default story
export const Default = Template.bind({});
// Dt Input -> With Warning story
export const WithWarning = Template.bind({});
// Dt Input -> With Error story
export const WithError = Template.bind({});

Writing Stories

To write stories, we first import the component that we wish to write stories for:

// input.stories.js
import DtInput from './input';

Then we will define the Default Export for the simplest use case (No configuration):

// input.stories.js
import DtInput from './input';

export default {
  title: 'Forms/Input',
  component: DtInput,
};

Title Field

The title field will tell Storybook to put the stories in a category called Components. We can add more levels of nesting by adding more /.

For example, Component/Folder/Input will result in the following:

Nothing will appear yet, this is because we now need to add our named exports.

Story Template

First we will define the harness that will render our component:

// input.stories.js
const Template = () => {
  return {
    components: { DtInput },
    template: <dt-input />,
  };
};

Since Template returns a Vue component that wraps our component, we need to pass through all of the props that our component expects so Storybook can interact with our component's internals using its UI:

// input.stories.js
const Template = () => {
  return {
    components: { DtInput },
    template: <dt-input :name="name" :value="value" ... />,
    props: [name, value ...],
  };
};

generateTemplate Helper Method

As you can imagine, depending on the component, this can get quite tedious, luckily, the Template method takes in 2 arguments. We can also use the generateTemplate method to bind all props for us:

// input.stories.js
/**
 * generateTemplate will take a component and generate a template
 * where all of it's props are mapped to values with the same name,
 * the customProps field let's the user pass in additional bindings if desired.
 *
 * So this will generate the string:
 * <dt-input
 *  :name="name"
 *  :type="type"
 *  :value="value"
 *  ...
 *  :placeholder="placeholder",
 * />
 */
const baseInputTemplate = generateTemplate(DtInput, {
  customProps: [
    ':placeholder="placeholder"',
  ],
});

/**
 * argTypes is an object that contains all of the props
 * listed in the component. So we can automatically bind
 * all of the props by simplying using `props: Object.keys(argTypes)`
 */
const Template = (args, { argTypes }) => {
  return {
    components: { DtInput },
    template: baseInputTemplate,
    props: Object.keys(argTypes),
  };
};

Vue Single-File Component Template

Another option is to use an actual Vue Single-File Component as your template. This can be useful if you are rendering a more complex component and you would prefer to have code highlighting and the ability to use any of the advanced features of a .vue component. Below is an example usage of this in a stories.js file:

Important Notes

In order to address a current storybook bug where props that share the same name as a slot are not displayed in the sidebar controls we must define the slots controls in argTypesData as seen in the example below.

import DtNotice from './notice';
import NoticeDefault from './NoticeDefault.story.vue';

export const argTypesData = {
  '#title': {
    name: 'title',
    description: 'slot for the notice title',
    table: {
      category: 'slots',
      type: {
        summary: 'text/html',
      },
    },
  },
};

export default {
  title: 'Elements/Notice',
  component: DtNotice,
  argTypes: argTypesData,
  excludeStories: /.Data$/,
};

const Template = (args, { argTypes }) => createTemplateFromVueFile(args, argTypes, NoticeDefault);

export const Default = Template.bind({});
Default.args = {
  title: 'Notice title',
  default: 'Main content of the notice goes here.',
  action: 'try this <a href="https://www.dialpad.com" target="_blank">action</a>',
};

And here is what the vue component NoticeDefault.story.vue looks like. Note that for these types of components that are used only for rendering components, we suffix them .story.vue, and keep them in the same folder as the corresponding stories.js file. In this case it is in the same folder as notice.stories.js:

<template>
  <dt-notice
    :kind="kind"
    :title="title"
    :title-id="titleId"
    :content-id="contentId"
    :important="important"
    :hide-close="hideClose"
  >
    <span v-html="defaultSlot" />
    <template
      v-if="action"
      #action
    >
      <span v-html="action" />
    </template>
    <template
      v-if="icon"
      #icon
    >
      <component :is="icon" />
    </template>
    <template
      v-if="titleOverride"
      #titleOverride
    >
      <span v-html="titleOverride" />
    </template>
  </dt-notice>
</template>

<script>
import DtNotice from './notice';
import icon from '../mixins/icon';

export default {
  name: 'NoticeDefault',
  components: { DtNotice },
  mixins: [icon],
};
</script>

Creating Named Exports / Setting Args

Now that we have the Template, we can create our named exports. Usually the Default named export represents our component in it's default state. Specifying the args property will on other named exports will create stories with those props set.

// input.stories.js
import DtInput from './input';
import { generateTemplate } from '@/common/storybook_utils';

export default {
  title: 'Forms/Input',
  component: DtInput,
};

const baseInputTemplate = generateTemplate(DtInput, {
  customProps: [
    ':placeholder="placeholder"',
  ],
});

const Template = (args, { argTypes }) => {
  return {
    components: { DtInput },
    template: baseInputTemplate,
    props: Object.keys(argTypes),
  };
};

// Default story
export const Default = Template.bind({});

// With Warning story with the 'messages' props set
export const WithWarning = Template.bind({});
WithWarning.args = {
  messages: [{ message: 'This is a warning message', type: 'warning' }],
};

Decorators

TODO

Ignoring Exports

Sometimes we don't want specific exports to be parsed as a story. This may occur if we want to export some data property to be reused in composite stories. This can be accomplished by specifying the excludeStories field in the default export

// input.stories.js

import DtInput from './input';

export default {
  title: 'Forms/Input',
  component: DtInput,
  parameters: { ... },
  decorators: [ ... ],
  excludeStories: /.*Data$/, // Ignores all named exports that end with "Data"
};

...

// Won't be parsed
export const propsData = {};

// Will be parsed
export const Default = Template.bind({});