Enhance your Stencil Web Components in Storybook with Knobs, Actions and JSX
Tags: stencil, storybook, webcomponents, javascript
Published: 2020-03-04 15:17:31

In the [previous part](https://dev.to/ofhouse/build-a-web-component-library-with-stencil-and-storybook-c27) of this series we got our setup of [Stencil](https://stenciljs.com/) and [Storybook](https://storybook.js.org/) up and running. This part goes a little deeper and we'll learn how to improve our stories by introducing Storybook's Knobs and Actions addons to our existing project.
Finally we'll learn how to simplify our stories by writing our Stencil Web Components directly in JSX rather than in plain JavaScript.

> **Note:** If you are here to learn how to write your stories in JSX, you can skip to the end of this article: [Use JSX to render your components in Storybook](#use-jsx).

# Adding Knobs and Action addons to the stack

To make a greater use of Storybook we first add the [Knobs](https://www.npmjs.com/package/@storybook/addon-knobs) and [Actions](https://www.npmjs.com/package/@storybook/addon-actions) addons to our existing setup. The Knobs addon provides us a simple interface to modify the properties of our Web Components on the fly. The Actions addon logs all events from our Web Component, so that we can see which events are emitted.

![Screen-Recording of Storybook with knobs and actions addons](https://dev-to-uploads.s3.amazonaws.com/i/opyprn2zq988lv8ihw3k.gif)

First we install the addons:

```sh
npm i --save-dev @storybook/addon-knobs @storybook/addon-actions
```

And then add them to our storybook configuration:

```diff
// .storybook/main.js

...
module.exports = {
  stories: ['../src/**/*.stories.js'],
  addons: [
    '@storybook/addon-notes/register',
+   '@storybook/addon-knobs/register',
+   '@storybook/addon-actions/register',
  ],
  ...
}
```

# Add a new component

To learn how to integrate the Storybook addons with a Web Component we start by creating a new button component called `<my-button>`. Again we use Stencil's built-in CLI here for creating a boilerplate for our new component:

```sh
npx @stencil/core generate
```

![Screenshot of the Stencil CLI for creating a new component - part 1](https://dev-to-uploads.s3.amazonaws.com/i/0ytkuu42bor7fkdgdkcq.png)
![Screenshot of the Stencil CLI for creating a new component - part 2](https://dev-to-uploads.s3.amazonaws.com/i/h6e61rzlrmdk6o91vqok.png)

After the CLI has finished creating the new files for our component, make sure to run a quick build so that Stencil also adds a simple readme to this component:

```sh
npm run build
```

Since our new button component is pretty empty right now we give it more content by replacing it with the following content:

```tsx
// my-button.tsx
import { Component, Prop, h, Event, EventEmitter } from '@stencil/core';

@Component({
  tag: 'my-button',
  styleUrl: 'my-button.css',
  shadow: true,
})
export class MyButton {
  @Event() onClick: EventEmitter;
  @Prop() label: string;

  handleClick(event: UIEvent) {
    this.onClick.emit(event);
  }

  render() {
    return <button onClick={this.handleClick.bind(this)}>{this.label}</button>;
  }
}
```

Our component now has a property `label` where we can define what text is shown on our button and an `onClick` event which is fired when the button is clicked.
Now we are ready to add a new story for this button by creating a new file called `my-button.stories.js` in the same directory:

```diff
src/
└── components/
    ├── my-component/
    └── my-button/
        ├── my-button.css
+       ├── my-button.stories.js
        ├── my-button.tsx
        └── readme.md
...
```

Inside of the file we now define our story:

```js
// my-button.stories.js

import { storiesOf } from '@storybook/html';
import { withKnobs, text } from '@storybook/addon-knobs';
import { withActions } from '@storybook/addon-actions';

import readme from './readme.md';

storiesOf('My Button', module)
  .addDecorator(withActions('onClick'))
  .addDecorator(withKnobs)
  .add(
    'Default',
    () => {
      const buttonElement = document.createElement('my-button');
      // Knob property Label
      buttonElement.value = text('Click Me!', 'Label');
      // Event handler for onClick
      buttonElement.onClick = () => {
        console.log('Clicked');
      };

      return buttonElement;
    },
    {
      notes: {
        markdown: readme,
      },
    }
  );
```

When we now start storybook we can now see that a new panel with our addons is added to the view:
![Screenshot of Storybook with the addon panel open](https://dev-to-uploads.s3.amazonaws.com/i/jf7yzsza1fz289csdbep.png)

# Use JSX to render your Web Components in Storybook <a name="use-jsx"></a>

Maybe you are wondering why we wrote the implementation of our Stencil components in JSX while our stories use plain JavaScript to initialize our components. Let's change that.
I saw the following tweet by [Surma](https://twitter.com/DasSurma/status/1232742089408073728) last week where he shows that with a little help of a babel transformer and 15 lines of code it is pretty easy to write DOM in JSX:

{% twitter 1232742089408073728 %}

There is already a tiny library out there called [`jsx-dom`](https://www.npmjs.com/package/jsx-dom) so we don't have to copy the code from the screenshot.

In order to make it work, we need to add 3 new packages to our setup:

```sh
npm i --save-dev @babel/plugin-syntax-jsx @babel/plugin-transform-react-jsx jsx-dom
```

Then we add a `.babelrc` file to the root of our project:

```js
// .babelrc

{
  "plugins": [
    "@babel/plugin-syntax-jsx",
    [
      "@babel/plugin-transform-react-jsx",
      {
        "pragma": "h"
      }
    ]
  ]
}
```

Now we are ready to rewrite the story for our button component in JSX. Simply make sure to add the pragma `import { h } from 'jsx-dom';` to the top of every story that uses JSX. We then can bind the properties to variables or inline them directly:

```jsx
// my-button.stories.js

import { storiesOf } from '@storybook/html';
import { withKnobs, text } from '@storybook/addon-knobs';
import { withActions } from '@storybook/addon-actions';
import { h } from 'jsx-dom';

import readme from './readme.md';

storiesOf('My Button', module)
  .addDecorator(withActions('onClick'))
  .addDecorator(withKnobs)
  .add(
    'Default',
    () => {
      const label = text('label', 'Label');

      return (
        <my-button
          label={label}
          onClick={() => {
            console.log('Clicked');
          }}
        ></my-button>
      );
    },
    {
      notes: {
        markdown: readme,
      },
    }
  );
```

---

_Thanks for reading. There are still some features which are missing in this setup (e.g. Hot-Module-Replacement) which I hopefully can figure out in the coming weeks. So feel free to follow me here for more updates on this topic!_

---

A live-demo of the Storybook is available here: https://ofhouse.github.io/storybook-stencil-example
If your are interested to try it out by yourself, you can check out the full repository on GitHub:
{% github ofhouse/storybook-stencil-example %}