Stencil + Storybook

TL;DR: With some effort, Stencil and Storybook can work together. Sample repository below.

The Setup

I have been using StencilJS for a couple of years now. While not perfect, it has a lot of features I really enjoy for a Web Component library (TypeScript, JSX, compiled). It’s a great framework for building a Web Component library and one that I have implemented with a few clients. However, one of the pain-points of creating a Stencil component library is the ability to demonstrate and document the component(s) features. This is always a must when I work on component libraries for clients. Clients need to see how a component behaves before it is integrated into an application, and we as engineers need to be able to test the component prior to integration. Additionally, engineers will need documentation on the different properties of a component and how to implement.

When you scaffold a Stencil component library project, it does create an index.html file for you to load your component(s) and view it in a browser. As you add components to your library, you now need to update this file for the additional components, or add additional HTML files to properly organize your code. Before you know it, you are now building a custom site to test, document, and demonstrate the components. Additionally, each component will likely have one or more of the following: Properties, Events, CSS Custom Properties, Slots. Now, each component may have multiple permutations of things you need in your HTML files. This approach become unsustainable rather quickly in a Stencil component library project.

Luckily, this is a problem that has been solved in the React community for quite some time. Enter StorybookJS.

StorybookJS is well known in the React community for being a great test and demonstration harness for component libraries. Additionally, it serves as an interactive documentation platform for components as well. The benefits of Storybook are well documented, especially for React, Angular, and other mainstream technologies. One area that has been lacking for Storybook has been Web Components. This has changed recently with their V6 release, which now contains a Web Component friendly implementation. However, it’s still not a perfect fit for Stencil.

To put it simply, there’s no native integration between Storybook and Stencil, but that doesn’t mean it’s impossible. If you’re comfortable with a Storybook application, you can make the two work together with a little bit of work. The rest of this post will guide you in integrating Storybook into a Stencil component library project.

The Steps

First, you need to create your Stencil project:

  1. npm init stencil
  2. Choose “Component”
  3. Name your project

Next, you will need to add Storybook to your project:

  1. npx -p @storybook/cli sb init
  2. Choose “web_components”

Next, you’ll need to bind in the output of the Stencil build into the preview frame of Storybook. Create .storybook/preview-head.html and add the following content:

<script type="module" src="/build/stencil-storybook-starter.esm.js"></script>
<script nomodule src="/build/stencil-storybook-starter.js"></script>

This file will load in the scripts into the Storybook preview pane.

Lastly, modify your package.json to update your “storybook” script. This will inform storybook to start up with a static assets folder:

"storybook": "start-storybook -p 6006 --no-dll -s ./www"

Once all your modifications are complete, you can simply run the Stencil build (`npm run build`) and then start Storybook. You should now see your Stencil components load within Storybook.

To write your story, you just need to instantiate your component via regular JavaScript and add the element to the page. But first, we need to take care of a couple of things. It becomes a little awkward to bind all the things we need (props, events, etc.) for the component, so I created a helper file to help bind properties and D.R.Y. up the code.

Helper:

export const bindProps = (el, props, args) => {
    Object.keys(args)
        .filter(x => props.includes(x))
        .forEach(x => el.setAttribute(x, args[x]));
};

export const bindEvents = (el, events, args) => {
    Object.keys(args)
        .filter(x => events.includes(x))
        .forEach(x => el.addEventListener(x, args[x]));
};

export const bindStyles = (el, styles, args) => {
    Object.keys(args)
        .filter(x => styles.includes(x))
        .forEach(x => el.style.setProperty(x, args[x]));
};


export const bindSlots = (el, slots, args) => {
    Object.keys(args)
        .filter(x => slots.includes(x))
        .forEach(x => el.innerHTML = el.innerHTML + args[x]);
};

Story:

import * as Utils from '../../stories/StencilStorybookUtils';

//
// SETUP
//
export default {
    title: 'Example/My Component',
    parameters: {
        actions: {
            handles: ['tapped']
        }
    },
    decorators: [
    ]
};

//
// DEFINE PROPERTIES, ETC FOR COMPONENT
//
const PROPS = ['first', 'middle', 'last'];
const EVENTS = ['tapped'];
const CSS_VARS = [
    '--border-size',
    '--border-color'
];
const SLOTS = ['one', 'two'];

//
// TEMPLATE
//
const Template = args => {
    const el = document.createElement('my-component');

    Utils.bindProps(el, PROPS, args);
    Utils.bindEvents(el, EVENTS, args);
    Utils.bindStyles(el, CSS_VARS, args);
    Utils.bindSlots(el, SLOTS, args);

    return el;
}

//
// STORIES
//
export const Primary = Template.bind({});
Primary.args = {
    first: 'Homer',
    last: 'Simpson',
    tapped: (e) => console.log(e.detail),
    '--border-size': '1px',
    '--border-color': 'red',
    one: '<span slot="one">ONE</span>',
    two: '<span slot="two">TWO</span>'
}

export const Secondary = Template.bind({});
Secondary.args = {
    first: 'Bart',
    last: 'Simpson',
    '--border-size': '5px',
    '--border-color': 'lime',
}

 

Conclusion

Creating a component library is a lot of work, but the benefits can be great for a team looking for highly reusable widgets to accelerate their projects. StencilJS can easily fit this requirement. By integrating Storybook into your Stencil project, you have made your component library interactive, self documenting, demonstrable, and testable.

It is possible to integrate StencilJS components into a Storybook with some extra steps. Once complete, you’ll have a nicely integrated component library–your clients and engineers will thank you!

A sample repository showing how to integrate StencilJS and Storybook is below. Feel free to use this to bootstrap your own projects!

Repository:
https://github.com/hartjus/stencil-storybook-starter