Nuxt環境でStorybook 6.0をUIカタログからステップアップさせる: Docs(未完成), Controls

この記事は「[株式会社オープンストリーム "小ネタ" Advent Calendar 2020](https://qiita.com/advent-calendar/2020/opst-mini)」の 8 日目の記事です。

[前記事](https://qiita.com/ysd_marrrr/items/9a4038a150c179744138)で Storybook6.0 の導入を開設しましたが、Storybook の機能をさらに使って UI カタログ以上の使い方をしましょう。

## 環境

- Nuxt.js 2.14.9
- Storybook 6.0.10

```
$ node -v
v14.5.0

$ yarn -v
1.22.5
```

## Storybook にアドオンを追加する

`sb init` したときに次のアドオンが自動的に導入されています。

```json:package.json
...
  "devDependencies": {
    "@storybook/addon-actions": "^6.1.10",
    "@storybook/addon-essentials": "^6.1.10",
    "@storybook/addon-links": "^6.1.10",
...
```

"devDependencies" にアドオンのパッケージが導入されていなければ、 `yarn add -D` で導入します。

```bash
yarn add -D @storybook/addon-essentials
```

パッケージを add した後は `.storybook/main.js` に使いたいアドオンを読み込みます。

```js:.storybook/main.js
const path = require('path')

module.exports = {
  addons: ['@storybook/addon-essentials'],

...
```

そのほかに、パッケージ add 後 `.storybook/preview.js` に記述する必要があるアドオンなどもありますので、README を読んでから導入しましょう。

## Docs

`@storybook/addon-essentials` を導入すると使えるようになります。Storyで使っているコンポーネントについて `props`, `methods` などに説明を追加することができます。

### Nuxt(Vue)の環境でMDXを書くことができなかった

Storybook のドキュメントの通り Writing Stories の次の Writing Docs を進めようとすると、`create-nuxt-app`の環境だと [MDX を書く段階](https://storybook.js.org/docs/react/writing-docs/mdx) で Vue コンポーネントを読み込んでくれないので別の方法で Storybook のドキュメントを作成します。

> WARNING in ./components/buttons.stories.mdx 22:13-26
> "export 'CustomButtons' was not found in './CustomButtons.vue'


### VueコンポーネントからDocsを自動生成する

Story ファイルと Vue コンポーネントから自動生成する方法が別に用意されています。 `vue-docgen-api` (依存関係で導入される)を使った仕組みで、Storybook 5.3 から使えます。

その前に、Story ファイルでは Story でコンポーネントを呼び出すときに `prop` を受け入れる形に書き直す必要があります。
これをやらないと "Nothing found" と表示されたままになります。

Storyファイル内でTemplateを定義してからそのTemplateをbind, そのあとに `args` プロパティに `props` の値をセットします。

```ts:.storybook/main.js
import CustomButtons from './CustomButtons.vue'
export default {
  title: 'Button'
}

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { CustomButtons },
  template: `<custom-buttons v-bind="$props" /> `
})

export const FirstTime = Template.bind({})
FirstTime.args = {
  isVisibleTutorButton: true,
  colorMode: 'light',
}

export const Normal = Template.bind({})
Normal.args = {
  isVisibleTutorButton: false,
  colorMode: 'light',
}
```

そして、Vue コンポーネント側に `vue-docgen-api` で認識できる形に注釈を入れるのですが…

```vue:components/CustomButtons.vue
<template>
  <div>
    <p class="introduction">好きな<strong>処理</strong>を選択してください</p>
    <div class="buttons">
      <button @click="onClick" class="button" v-if="isVisibleTutorButton">
        tutor
      </button>
      <button @click="onClick" class="button is-primary">かんたん</button>
      <button @click="onClick" class="button is-link">むずかしい</button>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
export default Vue.extend({
  props: {
    /**
     * チュートリアルのボタンを表示させるか
     */
    isVisibleTutorButton: {
      type: Boolean,
      default: true,
    },
  },
  methods: {
    /**
     * ボタンがクリックされたとき
     * @Event onClick
     * @type {Object}
     */
    onClick() {
      this.$emit("onButtonClick");
    },
  },
});
</script>
```

~~締め切り過ぎてまで~~ 検証しても Description に説明を挿入できませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/a4422142-020b-b898-acc6-1bea2aa236db.png)

また、`props`, `methods` などに付けたアノテーション以外に説明文を追加する方法は見つかりませんでした。


## Controls addon

ドキュメントでだいぶ出鼻をくじかれた形になりますが、他のアドオンを使ってみましょう。

こちらも `@storybook/addon-essentials` を導入すると使えるようになります。

Story でコンポーネントを呼び出すときに、コンポーネントに props があってそれが Story から args として渡されていれば、自動的にそれを Storybook で変更できるようにするアドオンになります。

例としてVueコンポーネントにはBoolean, Stringの `props` を2つ追加します。

```vue:components/CustomButtons.vue
<template>
  <div>
    <p class="introduction">好きな<strong>処理</strong>を選択してください</p>
    <div class="buttons">
      <button
        @click="onClick"
        class="button"
        :class="colorClass"
        v-if="isVisibleTutorButton"
      >
        tutor
      </button>
      <button @click="onClick" class="button is-primary">かんたん</button>
      <button @click="onClick" class="button is-link">むずかしい</button>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
export default Vue.extend({
  props: {
    isVisibleTutorButton: {
      type: Boolean,
      default: true,
    },
    colorMode: {
      type: String,
      default: "light",
      validator: function (value) {
        return ["light", "dark", "black"].indexOf(value) !== -1;
      },
    },
  },
  methods: {
    onClick() {
      this.$emit("onButtonClick");
    },
  },
  computed: {
    colorClass() {
      return "is-" + this.colorMode;
    },
  },
});
</script>
```

Storyファイルでその `props` に対して値を入れるとStorybookを開いたときにControlsが表示されます。

```ts:components/buttons.stories.ts
import CustomButtons from './CustomButtons.vue'
export default {
  title: 'Button'
}

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { CustomButtons },
  template: `<custom-buttons v-bind="$props" /> `
})

export const FirstTime = Template.bind({})
FirstTime.args = {
  isVisibleTutorButton: true,
  colorMode: 'light',
}

export const Normal = Template.bind({})
Normal.args = {
  isVisibleTutorButton: false,
  colorMode: 'light',
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/3a896d08-6828-8296-d723-15c59a870296.png)


### Controls を手動で定義する

ただし、自動的に作ってくれるとはいえ Storybook 上でVueの `props` の形式に対して不適切なコントロールが作られることがあります。

例えばボタンをダークモード向けに暗くする設定を `colorMode` というpropに仕込むとします。

`is-light`, `is-dark`, `is-black` の 3 種類の値をとるpropの場合でも、Controls addon のデフォルトではテキストボックスが表示されてしまいます(先ほどの例も `colorMode` がテキストボックスになってます)

これをテキストボックスではなく 3 つの選択肢に制限したい時や、テキストボックスよりももっと良いコントロールを使いたい場合は **Story ファイルに Controls addon の設定を `argTypes` に記述します。**

コントロールの種類は `types` で指定します。 [こちら](https://storybook.js.org/docs/react/essentials/controls#annotation) に載っています。

```ts:components/buttons.stories.ts
import CustomButtons from './CustomButtons.vue'

export default {
  title: 'Button',
  argTypes: {
    colorMode: {
      control: {
        type: 'inline-radio',
        options: ['light', 'dark', 'black']
      }
    }
  }
}

...
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/518b2213-209e-793c-c917-a38cc10b63af.png)


### Controls を使用する想定でない Story の場合

Controls addon を導入した場合、Controls を表示できる設定になっていないと次のメッセージが表示されます。

> This story is not configured to handle controls. Learn how to add controls »

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/d712611d-90fd-71b5-4987-2ede3be75cd8.png)


後で Controls を使う想定であればそのままにしておきますが、Controls を使いたくない Story の場合は Story ファイルで Template を bind した後 `hideNoControlsWarning: true` を parameters に設定することでこの表示を消すことができます。

(Controls タブを消すことはできません)

```ts:components/simple-buttons.stories.ts
import SimpleButton from "./SimpleButton.vue";

export default {
  title: "Simple Button",
};

const SimpleTemplate = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { SimpleButton },
  template: `<simple-button /> `,
});

export const ControlsDisabled = SimpleTemplate.bind({});
ControlsDisabled.parameters = {
  controls: { hideNoControlsWarning: true },
};
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/b90f2285-c299-50a0-1ab5-a1021f322d31.png)


また、 `export default` の部分で `argTypes` が設定されていると、この設定にかかわらず Controls が表示されます。

Story 単位で Controls を完全に無効にする設定ではなく、あくまでも Controls を使用する想定ではない Story であることを設定するものです。

## 参考

How to document components
https://storybook.js.org/docs/react/writing-docs/introduction

[アドオン編] Essential addons について｜ Vue と CSF によるモダンな Storybook 6 のはじめかた
https://zenn.dev/sa2knight/books/aca5d5e021dd10262bb9/viewer/1c420c

Storybook for Vue の Docs Addon の裏側: コンポーネントの自動ドキュメント生成について - log.pocka.io
https://log.pocka.io/posts/vue-docgen-loader/