storybook6のcontrolsまとめ

## はじめに
本記事では，UIカタログとして利用されるstorybookについて記述します．
storybookにはcontrolsと呼ばれる便利機能があり，コンポーネントなどに与えるデータやプロパティをstorybook上で簡単に変更することができます．新たに作ったUIコンポーネントのテストはもちろん，すでに作られたコンポーネントの挙動を理解するためにも大いに役立ちます．
![sample1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/4b2ddd04-a485-ea7a-db48-a70533357e87.gif)
storybookのcontrols機能では，前述のように，UIコンポーネントにデータやプロパティを変更できる機能のことです．ここに貼ったgif画像では，bool値であるprimaryの値をスライドボタンで切り替えたり，ボタンのラベルを変更しています．storybookではこのような値の変更方法以外にも，様々な方法が用意されています．本記事はVue.js環境におけるこれらの記述方法を備忘録的にまとめたものです．
本記事では，あくまでcontrolsの記述方法にフォーカスしていますので，storybookそのもののセットアップ方法や概要などを詳しく知りたい方は，[公式ドキュメント](https://storybook.js.org/docs/react/get-started/introduction)や，他の方の記事を参照してください．他の方の記事ですが以下の記事は個人的にとてもわかりやすかったです．
参考記事：[Vue と CSF によるモダンな Storybook 6 のはじめかた](https://zenn.dev/sa2knight/books/aca5d5e021dd10262bb9)

## どんな入力方法があるのか？
storybook公式ドキュメントの[controlsの項目](https://storybook.js.org/docs/react/essentials/controls)では，以下のようにまとめられています．
![スクリーンショット 2021-08-02 22.09.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/ab96cb9d-01ae-e44a-51c2-50f1d1620ee4.png)
15種類のControl Typeがあることがわかります．
以降から，storybookをインストールした直後の環境で動くサンプルコードを載せていきます．作成するファイルは全て`src/stories/`配下に置いていきます．
それでは一つづつ記述方法と動作を確認してみましょう．

## boolean
### boolean
booleanでは，文字通りtrue，falseの値を切り替えることができるcontrolです．
動作確認用に，bool値を受け取る，flagというpropsを持つsampleコンポーネントを作成します．flagに渡される値によって表示される文字が変化します．

```vue:sample.vue
<template>
  <div>
    <p v-if=flag>true</p>
    <p v-else>false</p>
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    flag: {
      type: Boolean,
      required: true,
    },
  },
};
</script>
```

次に，sample.vueを呼び出すstories.jsファイルを作成します．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  flag: true, // デフォルト値
};
```
実際に動作確認してみます．
![sample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/07bb682c-208c-7c32-1c3a-84ba4ade5f7f.gif)
flagのトグルボタンによってコンポーネントへ渡されているデータが変化していることがわかります．`<sample v-bind="args" />`でargsを渡すことで，`flag: true`のデフォルト値を渡しているのです．しかし，実際のところ，最後のPrimary.argsは記述せずともほぼ同様に動作します．これは，storybookが非常に賢いために，呼び出されているsample.vueに定義されているpropsを解析して，その型に適した入力方法を表示してくれるのです．つまり，NumberやString，Objectなどの型を指定したpropsなどは，storiesに呼び出してargsをv-bindするだけで利用できるようになります．

## Number
### Number
前項のsample.vueを少し書き加えてみましょう

```vue:sample.vue
<template>
  <div>
    <p v-if=flag>true</p>
    <p v-else>false</p>
    {{ num }} <!-- 追記部分 -->
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    flag: {
      type: Boolean,
      required: false,
    },
    num : { // 追記部分
      type: Number,
      default: 0,
    }
  },
};
</script>

```

propsに数字を受け取るように書き換えました．実行結果はこんな感じ．
![sample3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/26ebc511-94fd-499a-afde-72b9f7344c48.gif)

### Number(range)
rengeでは，Number型のインプットをスライドバー形式で操作ができるようになります．通常のNumberとは異なり，必ずstoriesファイルで追記が必要になります．

```vue:sample.vue
<template>
  <div>
    {{ rangeNum }}
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    rangeNum : {
      type: Number,
      default: 0,
    }
  },
};
</script>

```

storiesファイルのargTypesプロパティを指定します．今回は，rangeNumという変数を`control type:'range'`で指定します．デフォルト値を指定したい場合はargsで指定してあげましょう．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  // ここの記述がポイント
  argTypes: {
    rangeNum: {
      control: {
        type: 'range',
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  rangeNum: 0, // デフォルト値
};
```
![sample5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/d3cf05cd-5a17-8b00-5011-00741a2c0109.gif)

## Object
### Object
コンポーネントにObjectを受け取るpropsを用意し，storiesファイルでObject型の変数を渡してあげます．argTypesの記述は必要ありません（もちろん明示的に記述しても構いません）．

```vue:sample.vue
<template>
  <div>
    {{ sampleObject }}
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    sampleObject: {
      type: Object,
      required: false,
      default: {},
    },
  },
};
</script>
```

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  sampleObject: {
    apple: 'red',
    grape: 'purple',
  },
};
```
実行結果はこんな感じ
![sample6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/b83240dd-9a2c-3e12-f404-b8e001ff2221.gif)

## Array
### Array
ArrayもObjectと同様に，Array型の変数を受け取るpropsを用意してあげて，storiesファイルでpropsにArray型の変数を渡してあげます．

```vue:sample.vue
<template>
  <div>
    {{ sampleArray }}
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    sampleArray: {
      type: Array,
      required: false,
      default: [],
    },
  },
};
</script>
```

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  sampleArray: ['a', 'b', 'c'],
};
```
実行結果はこんな感じ
![sample7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/34aed6cc-6e07-a4c8-1b7a-987165a0947f.gif)

### Array(file)
ファイルをinputすることもできます（あまり使う機会はなさそう）．
Arrayのサンプルコンポーネントをそのまま流用して，storiesファイルのみ書き換えていきます．fileの場合はargTypesを記述します．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  // ここがポイント
  argTypes: {
    sampleArray: {
      control: {
        type: 'file',
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  sampleArray: [],
};
```
実行結果はこんな感じ
![sample8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/695b76dd-e1a8-42d9-2f50-752d9cad1d30.gif)

## enum
enumではいわゆる選択式のデータの渡し方を記述できます．６種類の入力方法があります．この６種類は基本的に描き方が同様です．
###　radio
radioでは，storiesファイルのargTypesのtypeに`radio`を指定し，選択肢を`options`に配列として記述します．

```vue:sample.vue
<template>
  <div>
    {{ fruit }}
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    fruit: {
      type: String,
      required: false,
      default: '',
    },
  },
};
</script>
```

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'radio', // ここと，
        options: ['apple', 'banana', 'grape', 'orange'], // ここ
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/355e4c80-8b93-a299-ae85-c99bfbfb4f17.gif)

実際に渡したいデータにラベルを指定したい場合は，argTypesに`labels`を以下のようにオブジェクト形式で記述してあげると実現できます．vueファイルの変更はありません．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'radio',
        options: ['apple', 'banana', 'grape', 'orange'],
        labels: { // ここでラベルを指定する
          apple: 'りんご',
          banana: 'ばなな',
          grape: 'ぶどう',
          orange: 'オレンジ'
        },
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample10.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/27779e20-3912-0cc4-f352-c896f2560284.gif)

### inline-radio
radioの選択肢を選択肢ごとに改行せずに横並びに表示します．radioの記述方法と同様で，argTypesの`type`を`inline-radio`にすることで動作します．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'inline-radio', // 変えるのはここだけ
        options: ['apple', 'banana', 'grape', 'orange'],
        labels: {
          apple: 'りんご',
          banana: 'ばなな',
          grape: 'ぶどう',
          orange: 'オレンジ'
        },
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/a05a39c2-e3c2-bd12-32d4-959e6bca9601.gif)

選択肢の数が多い場合やラベルの文字列が長い場合は`radio`，そうでない場合は`inline-radio`を使うのがいいかと思います．

### check
`check`は`radio`と記述方法は同様ですが，複数選択が可能で，入力を配列としてコンポーネントに渡します．
vueファイルはradioと同様です．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'check', // 変えるのはここだけ
        options: ['apple', 'banana', 'grape', 'orange'],
        labels: {
          apple: 'りんご',
          banana: 'ばなな',
          grape: 'ぶどう',
          orange: 'オレンジ'
        },
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/3ad712fe-4218-76b6-878f-c392b6ad2064.gif)

### inline-check
checkの選択肢を選択肢ごとに改行せずに横並びに表示します．checkの記述方法と同様で，argTypesの`type`を`inline-check`にすることで動作します．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'inline-check', // 変えるのはここだけ
        options: ['apple', 'banana', 'grape', 'orange'],
        labels: {
          apple: 'りんご',
          banana: 'ばなな',
          grape: 'ぶどう',
          orange: 'オレンジ'
        },
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample13.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/a07fbbf1-50ad-828a-8048-2d0f62afd1ab.gif)
radio同様，選択肢の数が多い場合やラベルの文字列が長い場合は`check`，そうでない場合は`inline-check`を使うのがいいかと思います．

### select
selectでは，プルダウン形式で単一の選択肢を行うことができます．
他のenumと同様の記述方法で，argTypesの`type`を`select`にすることで動作します．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'select', // 変えるのはここだけ
        options: ['apple', 'banana', 'grape', 'orange'],
        labels: {
          apple: 'りんご',
          banana: 'ばなな',
          grape: 'ぶどう',
          orange: 'オレンジ'
        },
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample14.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/60543efa-8ac9-6a22-2ba9-05bcbb919191.gif)

radioとの使い分けは，より選択肢が多い場合にselect，そうでもない時にradioを使うのがいいかと思います．

### multi-select
selectのようなUIで，複数選択できるようにしたものがmulti-selectです．ｃｔｒｌキーを押しながら選択することで複数選択が出入るようになります．
選択されたデータは配列の形でコンポーネントに渡されます．
他のenumと同様の記述方法で，argTypesの`type`を`multi-select`にすることで動作します．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'multi-select', // 変えるのはここだけ
        options: ['apple', 'banana', 'grape', 'orange'],
        labels: {
          apple: 'りんご',
          banana: 'ばなな',
          grape: 'ぶどう',
          orange: 'オレンジ'
        },
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});

Primary.args = {
  fruit: 'apple',
};
```

実行結果はこんな感じ
![sample15.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/21011387-85bc-bb13-e2de-0dcb8efcc2d8.gif)




## string
### String
コンポーネントにtype: Stringのpropsを追加することで表示することができます.argTypesの指定は必要ありません（明示的に指定しても当然動作します．）．

```vue:sample.vue
<template>
  <div>
    {{ message }} <!-- 変更部分 -->
  </div>
</template>

<script>
export default {
  name: 'Sample',

  props: {
    message: { // 変更部分
      type: String,
      required: false,
      default: '',
    },
  },
};
</script>

```

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});
```
![sample4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/835c7f20-646d-59ab-9cec-2fe5ec117ac3.gif)

### color
colorを用いることで，カラーパレットを使ってカラーコードを指定することができるようになります．
argTypesでtypeに`color`を指定することで実現できます．
vueファイルはstringと同様です．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'color', // ここで指定
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});
```
実行結果はこんな感じ
![sample16.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/bfbbbc13-b390-0a96-1409-b9fb1abe2224.gif)

### date
dateを用いることでカレンダーから日付と時刻を選択することができるようになります．
コンポーネントに渡される値はUNIX Time形式になります．

argTypesでtypeに`date`を指定することで実現できます．
vueファイルはstringと同様です．

```javascript:sample.stories.js
import Sample from './sample.vue';

export default {
  title: 'Example/Sample',
  component: Sample,
  argTypes: {
    fruit: {
      control: {
        type: 'date', // ここで指定
      },
    },
  },
};

const Template = (args) => ({
  components: { Sample },
  setup() {
    return { args };
  },
  template: '<sample v-bind="args" />',
});

export const Primary = Template.bind({});
```
実行結果はこんな感じ
![sample17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/f4c51c7e-87a5-bf5f-1f57-ba1eea6f6f8d.gif)

## おわりに
この記事ではStoryBook6に用意されているconnrols addonを一通り動かしてみました．これらを組み合わせると非常にリッチなコンポーネントのカタログを作成することができます．
storybookをつかった開発にお役立ていただけたら幸いです．