【ハンズオン/初心者向け】Nuxt.jsでStorybookを使ってみよう！

# はじめに
Storybookは、UI開発用のツールです。
開発環境と分離された環境で UI コンポーネントを個別に作成し、コンポーネントの再利用やテストを容易にします。
複雑な開発スタックを起動したり、特定のデータをデータベースに強制したり、アプリケーション内を移動したりすることなく、UI全体を開発できます。

Storybookのメリットは以下の記事をご確認ください。
ググったら1番上にありました。

https://service.plan-b.co.jp/blog/tech/29109/

## 環境
・ "@storybook/addon-actions": "^6.2.9"
・ "@storybook/addon-essentials": "^6.2.9"
・ "@storybook/addon-links": "^6.2.9"
・ "@storybook/vue": "^6.2.9"

公式サイトはこちら

https://storybook.js.org/docs/vue/get-started/introduction/

## 学べること
・Storybookの使い方
・Font Awesomeの導入方法
・Propsを使って、子→親にmethodsを渡す方法

## インストール

```terminal:ターミナル
####### STEP1 #######
$ npx create-nuxt-app nuxt-storybook

####### STEP2 #######
## vue2（Nuxt.jsはこちらを使いましょう）
$ npx sb init

## vue3
$ npx sb@next init

####### STEP3 #######
$ npm run storybook
```

これで、Storybookをローカルで起動できるようになります。

## ディレクトリ構成
以下のようなフォルダやファイルが作成されます。

```
├── .storybook
│   ├── main.js
│   ├── preview.js
│   
├── stories
│   └── assets
│   │   ├── code-brackets.svg
│   │   └── (省略)
│   ├── button.css
│   ├── Button.vue
│   ├── Button.stories.js
│   ├── header.css
│   ├── Header.vue
│   ├── Header.stories.js
│   ├── page.css
│   ├── Page.vue
│   ├── Page.stories.js
│   ├── Introduction.stories.mdx
```

## storiesにコンポーネントがあってもむd
コンポーネントは、storiesじゃなくてcomponentsに入れたいよね。
ということで、ファイルをすべて移動させましょう！
ついでにいらないLogo.vueも削除しときましょう。

ということで上記を実行すれば、このようなディレクトリ構造になります。

```
├── .storybook
│   ├── main.js
│   ├── preview.js
│   
├── components
│   └── assets
│   │   ├── code-brackets.svg
│   │   └── (省略)
│   ├── button.css
│   ├── Button.vue
│   ├── Button.stories.js
│   ├── header.css
│   ├── Header.vue
│   ├── Header.stories.js
│   ├── page.css
│   ├── Page.vue
│   ├── Page.stories.js
│   ├── Introduction.stories.mdx
│   ├── README.md
```

ただこのままだと使えません。
なので、.storybook/main.jsを修正しましょう。

```javascript:.storybook/main.js
module.exports = {
  "stories": [
    "../components/**/*.stories.mdx",
    "../components/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ]
}
```

これで完了です！
それでは再度Storybookを起動しましょう。

```terminal:ターミナル
npm run storybook
```

ちなみにこのままだとcssとvueが分割されています。
これもまとめた方がスマートだよねってことでまとめていきましょう！
ついでに、assetsもIntroductionもいらない気がするので削除しましょう。

ということで、ディレクトリ構造はこのようになりました。

```
├── .storybook
│   ├── main.js
│   ├── preview.js
│   
├── components
│   ├── Button.vue
│   ├── Button.stories.js
│   ├── Header.vue
│   ├── Header.stories.js
│   ├── Page.vue
│   ├── Page.stories.js
│   ├── README.md
```

ものすごくスッキリしましたね！

## 実際にコンポーネントを作ってみよう！

### まずは、SCSSをインストールしましょう

```terminal:ターミナル
npm install sass-loader node-sass --save-dev

## npm run devでバージョンで怒られたとき
## こんな感じで怒られました><
## WARN  sass-loader@11.0.1 is installed but ^10.1.1 is expected

npm uninstall sass-loader --save-dev
npm install sass-loader@10.1.1 --save-dev
```

次に、webpackのconfigを作成します。
これで、SCSSが使えるようになりました。

```javascript:.storybook/webpack.config.js
const path = require("path");

module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.s(a|c)ss$/,
    use: ["style-loader", "css-loader", "sass-loader"],
  });

  return config;
};
```

`参考記事`

https://zenn.dev/nus3/articles/db854d5fd7bfc1c3f39a

### 見出しデザインを作ってみよう
作成するものはこの2つです。

![スクリーンショット 2021-04-24 20.22.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/4aab4297-56d3-c330-5492-5decd9eb218c.png)

![スクリーンショット 2021-04-24 20.23.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/f1ba506e-54ea-0e05-0c23-24dab96305f0.png)

どちらも良くあるデザインですね。
それでは作っていきましょう！

#### Viewを作成
```vue:components/Atoms/HeadingH2.vue
<template>
    <div>
        <h2 :class="className">{{ title }}</h2>
    </div>
</template>

<script>
export default {
    name: 'Heading2',
    props: {
        title: {
            type: String,
            required: true,
        },
        className: {
          type: String,
          required: true
        }
    },
};
</script>

<style lang="scss" scoped>
.el-lv2Heading {
    padding: 1.5rem; /*文字の上下 左右の余白*/
    font-size: 1.75rem;
    color: #111111; /*文字色*/
    background: #f4f4f4; /*背景色*/
    border-left: solid 5px #0074d9; /*左線*/
    border-bottom: solid 3px #d7d7d7; /*下線*/
    font-weight: bold;
}

.el-lv2Heading-2 {
  position: relative;
  font-size: 1.75rem;
  padding: 1rem;
  text-align: center;
  font-weight: bold;

  &::before {
    position: absolute;
    bottom: -5px;
    left: calc(50% - 30px);
    width: 60px;
    height: 5px;
    content: '';
    border-radius: 3px;
    background: #0074d9;
  }
}
</style>
```

#### storiesファイル作成

```javascript:components/Atoms/HeadingH2.stories.js
// Viewをインポート
import HeadingH2 from "./HeadingH2.vue";

// Storybookで表示される内容
export default {
  title: "Components/Atoms/HeadingH2", //ディレクトリ
  component: "HeadingH2" //コンポーネント名
};

const Template = (args, { argTypes }) => ({
  components: { HeadingH2 }, //上記のコンポーネントと揃える
  props: Object.keys(argTypes),
  // templateで、下記の場合titleとclassNameに任意の値が入る
  template: "<HeadingH2 :title='title' :className='className' />"
})

// 1つ目のデザイン
export const Primary = Template.bind({});
Primary.args = { title: "見出し2です", className: "el-lv2Heading" };
Primary.storyName = "見出し2デザイン①";

// 2つ目のデザイン
export const Secondary = Template.bind({});
Secondary.args = { title: "見出し2です", className: "el-lv2Heading-2"};
Secondary.storyName = "見出し2デザイン②";
```

### それでは次にボタンを作ってみましょう！

まずボタンには2種類あります。
・リンクとして使うボタン
・@clickなど動作を行うために使うボタン

リンクの場合、`<nuxt-link>`を使う方が望ましく、
@clickの場合、`<button>`を使いたいです。

ということで同じボタンですが、分けてこの2つは作ります。

またデザインは3パターンです。
Controlsを使えば色の変更やボタンサイズ、ボタンタイプを変更できます。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/fb63fb8c-cfc0-4fe1-a808-23db32a94053.gif)

#### Font Awesomeを追加
##### npmでインストール

```terminal:ターミナル
// 必須（Solid Style）
npm install @fortawesome/vue-fontawesome @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons --save

// 任意（Regular Style, Brands Style）
npm install @fortawesome/free-regular-svg-icons  --save
npm install @fortawesome/free-brands-svg-icons  --save
```

##### プラグインを作成

```javascript:plugins/fontawesome.js
import Vue from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { config, library } from '@fortawesome/fontawesome-svg-core';
// solid Style
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
// regular Style
import {} from '@fortawesome/free-regular-svg-icons';
// brands Style
import {} from '@fortawesome/free-brands-svg-icons';

// nuxt.config.jsでCSSファイルを追加
config.autoAddCss = false;

// 利用するアイコンを配列に追加
const solidIcons = [faArrowRight];
const regularIcons = [];
const brandsIcons = [];

// 利用するアイコンをlibraryに追加
solidIcons.forEach((icon) => library.add(icon));
regularIcons.forEach((icon) => library.add(icon));
brandsIcons.forEach((icon) => library.add(icon));

// グローバルコンポーネントに登録
Vue.component('fa', FontAwesomeIcon);
```

##### nuxt.config.jsに追記
```javascript:nuxt.config.js
export default {
    css: ['@fortawesome/fontawesome-svg-core/styles.css'],
    plugins: [
        '@/plugins/fontawesome',
    ],
};
```

##### Storybookに読み込む
```javascript:.storybook/preview.js
import { configure } from '@storybook/vue';
import Vue from 'vue';

import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
library.add(fas, far, fab);

Vue.component('fa', FontAwesomeIcon);

configure(require.context('../components', true, /\.stories\.js$/), module);
export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};
```

##### Componentsで使用する
```vue:components/Atoms/LinkButton.vue
<template>
  <div>
    <span v-if="isIcon">
      <fa :class="{ icon: isIcon }" :icon="[iconType, iconName]" />
    </span>
  </div>
</template>

<script>
export default {
  name: 'RoundButton',
  props: {
    isIcon: {
      type: Boolean,
      default: false,
      validator: function (value) {
        return [true, false].indexOf(value) !== -1;
      },
    },
    iconType: {
      type: String,
      required: false,
      validator: function (value) {
        return ['fas', 'fab', 'far'].indexOf(value) !== -1;
      },
    },
    iconName: {
      type: String,
      required: false,
    },
  },
  computed: {
    classes() {
      return {
        'icon-btn': this.isIcon,
      };
    },
  },
};
</script>
```

これで、Font Awesomeが使えるようになりました。

#### Viewを作成
```vue:components/Atoms/LinkButton.vue
<template>
  <div>
    <nuxt-link :to="link" :class="classes">
      {{ btnName }}
      <span v-if="isIcon">
        <fa :class="{ icon: isIcon }" :icon="[iconType, iconName]" />
      </span>
    </nuxt-link>
  </div>
</template>

<script>
export default {
  name: 'RoundButton',
  props: {
    btnName: {
      type: String,
      required: true,
    },
    link: {
      type: String,
      default: '/',
    },
    btnColor: {
      type: String,
      default: 'blue',
      validator: function (value) {
        return (
          [
            'navy',
            'blue',
            'olive',
            'green',
            'orange',
            'red',
            'maroon',
            'fuchsia',
            'purple',
            'black',
            'gray',
          ].indexOf(value) !== -1
        );
      },
    },
    btnType: {
      type: String,
      default: 'normal',
      validator: function (value) {
        return ['normal', 'rounded-corners', 'round'].indexOf(value) !== -1;
      },
    },
    btnSize: {
      type: String,
      default: 'medium',
      validator: function (value) {
        return ['small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1;
      },
    },
    isIcon: {
      type: Boolean,
      default: false,
      validator: function (value) {
        return [true, false].indexOf(value) !== -1;
      },
    },
    iconType: {
      type: String,
      required: false,
      validator: function (value) {
        return ['fas', 'fab', 'far'].indexOf(value) !== -1;
      },
    },
    iconName: {
      type: String,
      required: false,
    },
  },
  computed: {
    classes() {
      return {
        'el-btn': true,
        [`${this.btnColor}`]: true,
        [`el-btn-type-${this.btnType}`]: true,
        [`el-btn-size-${this.btnSize}`]: true,
        'icon-btn': this.isIcon,
      };
    },
  },
};
</script>

<style lang="scss" scoped>
// 共通レイアウト
.el-btn {
  display: inline-block;
  text-align: center;
  color: #fff;
  text-decoration: none;
  transition: 0.25s;
  cursor: pointer;
}
// ボタンタイプ
.el-btn-type-normal {
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
}
.el-btn-type-rounded-corners {
  border-radius: 10px;
}
.el-btn-type-round {
  border-radius: 50px;
}

// ボタンサイズ
.el-btn-size-small {
  font-size: 14px;
  padding: 10px 24px;
}
.el-btn-size-medium {
  font-size: 16px;
  padding: 12px 28px;
}
.el-btn-size-large {
  font-size: 18px;
  padding: 14px 32px;
}
.el-btn-size-xlarge {
  font-size: 20px;
  padding: 16px 36px;
  width: 250px;
  max-width: 100%;
}

//アイコン
.icon-btn {
  position: relative;
  padding-right: 2em;
  padding-left: 1.38em;
}
.icon {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 0.83em;
  font-weight: 900;
}

//ボタンの色
.navy {
  background-color: #001f3f;
  border: 2px solid transparent;
  border-bottom-color: #000e1d;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #001f3f;
  }
}
.blue {
  background-color: #0074d9;
  border: 2px solid transparent;
  border-bottom-color: #0062b7;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #0074d9;
  }
}
.olive {
  background-color: #3d9970;
  border: 2px solid transparent;
  border-bottom-color: #33815e;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #3d9970;
  }
}
.green {
  background-color: #2ecc40;
  border: 2px solid transparent;
  border-bottom-color: #28b037;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #2ecc40;
  }
}
.orange {
  background-color: #ff851b;
  border: 2px solid transparent;
  border-bottom-color: #f87300;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #ff851b;
  }
}
.red {
  background-color: #ff4136;
  border: 2px solid transparent;
  border-bottom-color: #ff2114;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #ff4136;
  }
}
.maroon {
  background-color: #85144b;
  border: 2px solid transparent;
  border-bottom-color: #67103a;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #85144b;
  }
}
.fuchsia {
  background-color: #f012be;
  border: 2px solid transparent;
  border-bottom-color: #d30da6;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #f012be;
  }
}
.purple {
  background-color: #b10dc9;
  border: 2px solid transparent;
  border-bottom-color: #950ba9;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #b10dc9;
  }
}
.black {
  background-color: #111111;
  border: 2px solid transparent;
  border-bottom-color: #000;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #111111;
  }
}
.gray {
  background-color: #aaaaaa;
  border: 2px solid transparent;
  border-bottom-color: #999999;

  &:focus,
  &:hover {
    background-color: #fff;
    border-color: currentColor;
    color: #aaaaaa;
  }
}
</style>
```

#### storiesファイル作成

```javascript:components/Atoms/LinkButton.stories.js
import LinkButton from './LinkButton.vue';

export default {
  title: 'Components/Atoms/LinkButton',
  component: 'LinkButton',
  argTypes: {
    btnColor: {
      control: {
        type: 'select',
        options: [
          'navy',
          'blue',
          'olive',
          'green',
          'orange',
          'red',
          'maroon',
          'fuchsia',
          'purple',
          'black',
          'gray',
        ],
      },
    },
    btnType: { control: { type: 'select', options: ['normal', 'rounded-corners', 'round'] } },
    btnSize: { control: { type: 'select', options: ['small', 'medium', 'large', 'xlarge'] } },
    isIcon: { control: { type: 'select', options: [true, false] } },
    iconType: { control: { type: 'select', options: ['fas', 'far', 'fab'] } },
  },
};

const Template = (args, { argTypes }) => ({
  components: { LinkButton },
  props: Object.keys(argTypes),
  template: "<LinkButton :btnName='btnName' :to='to' v-bind='$props' />",
});

export const Primary = Template.bind({});
Primary.args = {
  btnName: 'ボタン①',
  link: '/',
  btnColor: 'olive',
  btnType: 'normal',
  btnSize: 'xlarge',
  isIcon: true,
  iconType: 'fas',
  iconName: 'arrow-right',
};
Primary.storyName = 'ノーマルボタン';

export const Secondary = Template.bind({});
Secondary.args = {
  btnName: 'ボタン②',
  link: '/',
  btnColor: 'blue',
  btnType: 'rounded-corners',
  btnSize: 'large',
};
Secondary.storyName = '角丸ボタン';

export const Tertiary = Template.bind({});
Tertiary.args = {
  btnName: 'ボタン③',
  link: '/',
  btnColor: 'orange',
  btnType: 'round',
  btnSize: 'medium',
};
Tertiary.storyName = '丸ボタン';

// export const Quaternary = Template.bind({});
// export const Quinary = Template.bind({});
// export const Senary = Template.bind({});
// export const Septenary = Template.bind({});
// export const Octonary = Template.bind({});
// export const Novenary = Template.bind({});
// export const Decenary = Template.bind({});
```

これで完成です！

### methodsボタンを作成
次にmethodsなどで使うボタンを作成していきましょう。
デザインは先ほど使ったものをそのまま転用していきます。

#### Viewを作成

```vue:components/Atoms/MethodsButton.vue
<template>
  <div>
    <button :class="classes" @click="onClick()">
      {{ btnName }}
      <span v-if="isIcon">
        <fa :class="{ icon: isIcon }" :icon="[iconType, iconName]" />
      </span>
    </button>
  </div>
</template>

<script>
export default {
  name: 'MethodsButton',
  props: {
    btnName: {
      type: String,
      required: true,
    },
    btnColor: {
      type: String,
      default: 'blue',
      validator: function (value) {
        return (
          [
            'navy',
            'blue',
            'olive',
            'green',
            'orange',
            'red',
            'maroon',
            'fuchsia',
            'purple',
            'black',
            'gray',
          ].indexOf(value) !== -1
        );
      },
    },
    btnType: {
      type: String,
      default: 'normal',
      validator: function (value) {
        return ['normal', 'rounded-corners', 'round'].indexOf(value) !== -1;
      },
    },
    btnSize: {
      type: String,
      default: 'medium',
      validator: function (value) {
        return ['small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1;
      },
    },
    isIcon: {
      type: Boolean,
      default: false,
      validator: function (value) {
        return [true, false].indexOf(value) !== -1;
      },
    },
    iconType: {
      type: String,
      required: false,
      validator: function (value) {
        return ['fas', 'fab', 'far'].indexOf(value) !== -1;
      },
    },
    iconName: {
      type: String,
      required: false,
    },
    onClick: {
      type: Function,
      required: true,
    },
  },
  computed: {
    classes() {
      return {
        'el-btn': true,
        [`${this.btnColor}`]: true,
        [`el-btn-type-${this.btnType}`]: true,
        [`el-btn-size-${this.btnSize}`]: true,
        'icon-btn': this.isIcon,
      };
    },
  },
};
</script>
```

#### storiesファイル作成

```js:components/Atoms/MethodsButton.stories.js
import MethodsButton from './MethodsButton.vue';

export default {
  title: 'Components/Atoms/MethodsButton',
  component: 'MethodsButton',
  argTypes: {
    btnColor: {
      control: {
        type: 'select',
        options: [
          'navy',
          'blue',
          'olive',
          'green',
          'orange',
          'red',
          'maroon',
          'fuchsia',
          'purple',
          'black',
          'gray',
        ],
      },
    },
    btnType: { control: { type: 'select', options: ['normal', 'rounded-corners', 'round'] } },
    btnSize: { control: { type: 'select', options: ['small', 'medium', 'large', 'xlarge'] } },
    isIcon: { control: { type: 'select', options: [true, false] } },
    iconType: { control: { type: 'select', options: ['fas', 'far', 'fab'] } },
  },
};

const Template = (args, { argTypes }) => ({
  components: { MethodsButton },
  props: Object.keys(argTypes),
  template: "<MethodsButton :btnName='btnName' v-bind='$props' />",
});

export const Primary = Template.bind({});
Primary.args = {
  btnName: 'ボタン①',
  btnColor: 'olive',
  btnType: 'normal',
  btnSize: 'xlarge',
  isIcon: true,
  iconType: 'fas',
  iconName: 'arrow-right',
  onClick: '',
};
Primary.storyName = 'ノーマルボタン';

export const Secondary = Template.bind({});
Secondary.args = {
  btnName: 'ボタン②',
  btnColor: 'blue',
  btnType: 'rounded-corners',
  btnSize: 'large',
  onClick: '',
};
Secondary.storyName = '角丸ボタン';

export const Tertiary = Template.bind({});
Tertiary.args = {
  btnName: 'ボタン③',
  btnColor: 'orange',
  btnType: 'round',
  btnSize: 'medium',
  onClick: '',
};
Tertiary.storyName = '丸ボタン';

// export const Quaternary = Template.bind({});
// export const Quinary = Template.bind({});
// export const Senary = Template.bind({});
// export const Septenary = Template.bind({});
// export const Octonary = Template.bind({});
// export const Novenary = Template.bind({});
// export const Decenary = Template.bind({});

```

#### 親コンポーネントでの使い方

```vue:pages/index.vue
<template>
  <div class="container">
    <input type="text" v-model="test" />
    <!-- propsの呼び出し方法は、キャメルケースではなくケバブケース -->
    <!-- このようにonClickをon-clickに直さないと動作しない -->
    <MethodsButton
      btnName="送信"
      btnColor="blue"
      btnType="round"
      btnSize="small"
      :on-click="testMessage"
    />
  </div>
</template>

<script>
import MethodsButton from '@/components/Atoms/MethodsButton';
export default {
  components: {
    MethodsButton,
  },
  data() {
    return {
      test: '',
    };
  },
  methods: {
    testMessage() {
      console.log(this.test);
    },
  },
};
</script>
```

## 最後に

githubにあげました。

https://github.com/ssk9597/Nuxt-Storybook

また、Vue3やTypeScriptを使ってモダンに仕上げていく予定です。
そちらもいずれ記事にします！

ここまで読んでいただきありがとうございました。