Storybook で私とみんなのコンポーネントカタログ作成してこ

この記事は [Hamee Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hamee) 25日目の記事です。

私、入社したてのフロントエンド歴 4年目くらいの人です。
フロントエンドの環境とか技術とかインプットしたら、アウトプットもしないとなーと思っていたところでめでたくアドカレ初参加となりましたです👏

さてさっそく本題に…

## 私とみんなのコンポーネントカタログを作りたい！！

コンポーネント = HTML & CSS & JavaScript で作るパーツとして話を進めますね。

- こんなの作ったよ
- こんなコンポーネントあるんだよ
- 見た目こんなにあるんだよ

というのを、みんな (デザイナー、開発者、クライアント) に知ってもらう & わかってもらうために作りたい！
途中で他の方がお手伝いに来た時も、きちんと準備してあると、幸せですよね🤝

## じゃあ作ろう！

「はい、STOP」と、心の中のリトル自分が囁きます。

- おれおれテンプレートってどうなのよ
- フレームワークを使った時どうする？

まぁ確かに。
でも大丈夫。そう、**Storybook** ならね！

## Storybook
![hoge.gif](https://qiita-image-store.s3.amazonaws.com/0/63691/a89ed12e-c6cb-46ce-9df1-418887ea9dff.gif)
(👆この画像では　React 動かしてます)


[Storybook](https://storybook.js.org/) は、UI 開発環境です。
見た目はもちろん、挙動のテストも行えます。
アドオンで機能の追加も可能で、お好みの環境を作れることができます。

いつの間にやら対応するフレームワークやライブラリが増えていて、HTML だけってのもあるんです。

もちろん有名どころはバッチリ抑えてくれているので、Storybook さえ覚えれば、使用するライブラリで振り回されることがないですね！(最初の準備は大変だったりするんだけど…)

今回は、実際に私が使用した Storybook for HTML + Sass の小さい構成で、Storybook を紹介してみようと思います。

## 準備

- [Node.js](https://nodejs.org/ja/)

がインストールされている前提で進めていきます👍

基本的なことは [Storybook for HTML](https://storybook.js.org/basics/guide-html/) に書いてありますが、まずは `@storybook/cli` を使用して、雛形を作ってもらいましょう。

``` bash
npx -p @storybook/cli sb init --type html
```

終了したら下記コマンドを打って、動くか確認してみましょう！

``` bash
npm run storybook
```

<img width="1398" alt="capture1.png" src="https://qiita-image-store.s3.amazonaws.com/0/63691/b201ec43-74ae-69a1-2036-abacc2580896.png">

うん、動いてますね！

## 自分好みに変更

### 作業ディレクトリ

この段階でディレクトリ内がこうなってると思います。

    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── stories
    │   └── index.stories.js
    └── .storybook
        └── config.js

*index.stories.js* に確認の時に表示されたコードが書かれているのですが、そこに全部書いていくのも面倒ですし、コンポーネント開発用のディレクトリを作り、そちらで HTML & Sass を開発するようにしていきます。

    dev
    └── components

*dev/components* ディレクトリを作成し、そこで作業を進めていくことにします。
作業の場所が変更になるので、*.storybook/config.js* に書いてある、 `require.context` の第1引数を変更します。

``` javascript
import { configure } from '@storybook/html';

// dev ディレクトリ内にある  "なんちゃら.stories.js" というファイルを
// 自動でストーリーに追加してもらえるようにします。
const req = require.context('../dev', true, /.stories.js$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);
```

### Sass

Sass を使用するので [Custom Webpack Config](https://storybook.js.org/configurations/custom-webpack-config/) のページを参考に、Storybook の webpack 設定を変更します。

まずは必要なパッケージを追加します。

``` bash
npm i -D node-sass sass-loader css-loader style-loader
```

次は *./storybook* の中に *webpack.config.js* を下記内容で追加します。

``` javascript
const path = require('path');

module.exports = (storybookBaseConfig, configType) => {
  storybookBaseConfig.module.rules.push({
    test: /\.scss$/,
    loaders: ['style-loader', 'css-loader', 'sass-loader'],
    include: path.resolve(__dirname, '../')
  });

  return storybookBaseConfig;
};
```

ひとまずここで Storybook の設定は OK なので、実際にストーリーを書いていきましょう。

### ストーリー追加

Button という仮のコンポーネントのストーリーを作成していきましょう。

    dev
    └── components
        └── Button
            ├── button.stories.js
            └── _index.scss

``` scss
// _index.scss

@charset "UTF-8";

.Button {
  @at-root {
    & {
      padding: 1em;
      border: 2px solid currentColor;
      border-radius: 2px;
      font-weight: bold;
    }
  }
}

```

``` javascript
// button.stories.js

import './_index.scss';
import { storiesOf } from '@storybook/html';

storiesOf('Components', module)
  .add('button', () => `
    <button class="Button" type="button">ボタン</button>
  `);
```

ファイルを追加してうまくいくと…

<img width="1398" alt="capture2.png" src="https://qiita-image-store.s3.amazonaws.com/0/63691/35d6434e-e746-b59c-341d-71628f3d3d8f.png">

**Fuーッ！！**
ひだりのメニューに Components 、その下に button が追加され、右側にはきちんと追加したボタンが表示されています👏

### おまけ

今回の Storybook for HTML を使用した場合、テンプレートリテラルを使用して JS 内に HTMLを書くのですが、増えていった場合などを考えるとちょっとしんどいですよね。実際しんどかった、Emmet 効かないとか…。

なので、*html-loader* と *extract-loader* を使用して、HTML は別ファイルで管理してみましょう。

まずは *html-loader* と *extract-loader* を追加します。

``` bash
npm i -D html-loader extract-loader
```

*./storybook/webpack.config.js* もちょいと変更します。

```javascript
const path = require('path');

module.exports = (storybookBaseConfig, configType) => {
  storybookBaseConfig.module.rules.push(
    {
      test: /\.scss$/,
      loaders: ['style-loader', 'css-loader', 'sass-loader'],
      include: path.resolve(__dirname, '../')
    },

    // html 用のルールを追加
    {
      test: /\.html$/,
      loaders: ['extract-loader', 'html-loader'],
      include: path.resolve(__dirname, '../')
    }
  );

  return storybookBaseConfig;
};
```

こうすることで HTML ファイルを JS 内で文字列をして扱えるようになるので、
さっそく Button ディレクトリに HTML を追加しますー。

```html
<!-- default.html -->

<button class="Button"
        type="button">
  ボタン
</button>
```

```html
<!-- color.html -->

<button class="Button -red"
        type="button">
  レッド
</button>

<button class="Button -green"
        type="button">
  グリーン
</button>

<button class="Button -blue"
        type="button">
  ブルー
</button>
```

HTML ファイルを追加しましたら、Sass の方でモディファイアを追加します。

``` scss
// _index.scss

@charset "UTF-8";

.Button {
  @at-root {
    & {
      padding: 1em;
      border: 2px solid currentColor;
      border-radius: 2px;
      font-weight: bold;

      &.-red {
        color: red;
      }

      &.-green {
        color: green;
      }

      &.-blue {
        color: blue;
      }
    }
  }
}
```

準備ができたら JS の中で HTML を読み込んで使用してみましょう！
(Button の下に種類が表示されるようにメニューの構成も変更しています)

```javascript
// button.stories.js

import './_index.scss';
import { storiesOf } from '@storybook/html';
import defaultTemplete from './default.html';
import colorTemplete from './color.html';

storiesOf('Button', module)
  .add('default', () => defaultTemplete)
  .add('color', () => colorTemplete);
```

![output.gif](https://qiita-image-store.s3.amazonaws.com/0/63691/5e84a1c3-01c4-1c7d-cbcd-3ab3ab88421d.gif)

きちんと HTML ファイルが読み込まれましたね👏

## 最後に

ずらずら書いたのでわかりづらかったかもしれませんが、どうでしたでしょうか Storybook😅

半強制的にコンポーネントを開発しながらカタログを作っていくスタイルになるので、あとでドキュメント作成などで苦しむこともなくなるかなと思います👍
コマンド一発で静的ファイルを作成でき、PC 詳しくない人にみてもらう時にそのまま渡せちゃうのも良いですね🙆‍♂️

色々と試しながらドンドン Storybook を使っていこうと思います。