Nuxt.js(+Bulma) + StorybookでAtomic Designを実現する

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

「Vue.jsとAtomic Designを実践した」という事例が増えてきましたね✨
そこで今回はNuxt.jsとStorybookを用意しながらAtomic Designを実践したお話をできればなと思います。
  
(自明的に)この記事の対象としては次の3つになります。  

 * Nuxt.jsを使いたい(これ一つで完結したWebアプリを作りたい, SPAを簡単に導入したいなどなど)
 * Storybookを使いたい
 * Atomic Designを取り入れたい


## 開発環境

macOSと

```bash
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.1
BuildVersion:   19B88

$ node -v
v13.2.0

$ npm -v
6.13.1

$ yarn -v
1.21.0
```

Windowsで動作確認をしています。

```bash
C:\Windows>ver

Microsoft Windows [Version 10.0.18363.476]

...

$ node -v                       
v13.2.0                         
                                
$ npm -v                        
6.13.1                          
                                
$ yarn -v                       
1.21.0                          
```


## Atomic Design

はじめにUIを**一番小さい要素である**原子(Atoms)に分割して、それを下の図のように順番に組み合わせて意味のあるUIパーツやテンプレート(ページ)を作るデザイン手法です。

![Atomic Design.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/519b23d0-8f4e-41e8-4de0-a32211d21e94.png)
( http://atomicdesign.bradfrost.com/chapter-2/ より引用 )

(かなりざっくり言ってしまうと)例えばWebページでいうボタンやフォームのラベルなどがAtomsに該当します。

![atomic-design-parts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/710ff14c-db26-7a95-4d72-961058d4c77c.png)


小さい要素であるAtomsから順番に作っておくと、例えば新しい機能を作る中で「同じようなボタンを新しく作りたい」と感じたときは

 * すでにあるAtomsからMoleculesを作る
 * すでにあるOrganismsに別のデータ(文言など)を投入してTemplatesに追加して配置する

など **すでに書いたものを流用することができます。**


### Atomic Designの参考となる記事

Atomic Designについては、こちらの記事などがわかりやすいです。
https://design.dena.com/design/atomic-design-%E3%82%92%E5%88%86%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%A4%E3%82%82%E3%82%8A%E3%81%AB%E3%81%AA%E3%82%8B/

Atomic Designの元となる書籍はこちらから見ることができます。
http://atomicdesign.bradfrost.com/

「一番小さい単位であるAtomsではどのくらいまで分割すればいいのか」など、各コンポーネントの大きさについてはこちらがわかりやすいです。
https://speakerdeck.com/nrslib/vue-dot-js-and-atomic-design-guideline-for-components-division


### Nuxt.jsでAtomic Designを実現する際のファイル構成

NuxtでAtomic Designを実現するとき、Atoms/Molecules/Organisms/Templatesは `components` 以下に作るため**コンポーネント**として扱います。  

Atomsを組み合わせてMoleculesを作る…というように**コンポーネント間の親子関係が発生します。**  

![directory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/8d5db0cb-aece-0f8c-8a79-7b47c2640f3e.png)



## 作るサンプル

スライダーを動かすと「レビューらしきもの」の表示件数を変えることができて、「星を表示」の操作で各「レビューらしきもの」についている星を隠したりするサンプルを考えてみましょう。

![2019-12-05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/06b44fde-e224-5632-7a97-828e8464aa20.gif)

バックエンドのところは作りません。そこはNuxt.jsとか他のものでAPIを作って…
SPAのページを遷移してもデータを保持できるようにVuexを使って…も今はナシです🙅**後にしましょう**

サンプルコードはこちらに置いておきます。
https://github.com/ysd-marrrr/nuxt-atomic-design-20191205

## プロジェクトの作成

Nuxtのプロジェクトを作るときと同様です。 `create-nuxt-app` を使っています😄 

```
$ npx create-nuxt-app nuxt-atomic-design-20191205

create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in nuxt-atomic-design-20191205
? Project name nuxt-atomic-design-20191205
? Project description My learning Nuxt.js + Atomic Design Project
? Author name ysd-marrrr
? Choose the package manager Yarn
? Choose UI framework Bulma
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
```

componentsにAtoms/Molecules...を作成するため、ディレクトリを予め用意します、

```bash
$ mkdir -p ./components/{atoms,molecules,organisms,templates}
```

Atomic DesignでいうPagesはそのまま `./pages` に置きます。


## Storybookの設定

こちらもNuxtにStorybookを導入するときと同じようにします。
http://tacamy.hatenablog.com/entry/2019/05/27/113131

```
$ npx -p @storybook/cli sb init --type vue
```

Nuxtのコンポーネントをそのまま使いたいので、 `../components` を使うように `./.storybook/config.js` を修正します。

```js:./.storybook/config.js
import { configure } from '@storybook/vue';

configure(require.context('../components', true, /\.stories\.js$/), module);
```


Storybook導入時のサンプルは要らないので削除します。

```
$ rm -rf ./stories
```


## StorybookにBulmaのCSSを適用する

Nuxt用のWebpackとStorybook用のWebpackは別物なので、Storybook用の設定を追加します。
StorybookでもBulmaを使いたいので、ファイルを読み込めるよう`sass-loader`などを追加します。

```bash
$ yarn add -D node-sass sass-loader mini-css-extract-plugin
```

BulmaでWebpackを使うときと同様に**Storybookの`webpack.config.js`**を編集します。
https://bulma.io/documentation/customize/with-webpack/#3-5-create-a-webpack-config-webpack-4

```js:./.storybook/webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = ({ config }) => {
  config.resolve.alias['@'] = path.resolve(__dirname, '../')
  config.module.rules.push({
    test: /\.scss$/,
    use: [
        MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader'
        },
        {
          loader: 'sass-loader',
          options: {
            sourceMap: true
          }
        }
      ]
  });
  config.plugins.push(new MiniCssExtractPlugin({
    filename: 'css/mystyles.css'
  }));
  return config
}
```

**この設定を追加しないとStorybookを起動したときにコンポーネントが見つからない旨のエラーが出ます**

> ERROR in ./components/atoms/sliders/Slider.stories.js
> Module not found: Error: Can't resolve '@/components/atoms/sliders/Slider.vue' in '~/Projects/nuxt-atomic-design-20191205/components/atoms/sliders'
>  @ ./components/atoms/sliders/Slider.stories.js 2:0-59 6:14-20
>  @ ./components sync \.stories\.js$
>  @ ./.storybook/config.js
>  @ multi ./node_modules/@storybook/core/dist/server/common/polyfills.js ./node_modules/@storybook/core/dist/server/preview/globals.js ./.storybook/config.js (webpack)-hot-middleware/client.js?reload=true&quiet=true

また、MiniCssExtractPluginが設定されていないとStorybookにBulmaが適用できません。

私の環境ではこの後にcore-jsをダウングレードしないと core-jsの不具合でNuxtのビルドができなくなってしまったので、必要に応じでダウングレードします。
https://qiita.com/auau3700/items/27bd33ee8df6d3e505f4

```bash
$ yarn add core-js@2.6.9
```

次にStorybookにおける各コンポーネントでBulmaのCSSを読み込ませます。 `.storybook` 内に `CommonCss.vue` を作ります。

```vue:./.storybook/CommonCss.vue
<template>
  <div class="decorator">
    <slot name="story"></slot>
  </div>
</template>

<script>
  export default {
    name: 'CommonCss'
  }
</script>

<style lang="scss">
  @charset "utf-8";
  @import "~bulma/bulma";
</style>
```

`CommonCss.vue` を最初に読み込むように `config.js` を修正します。 **この読み込みでトラブルがあると「コンポーネント単体でSCSSが効かない」などおかしな挙動に襲われます(実体験)**

```js:./.storybook/config.js
import { configure, addDecorator } from '@storybook/vue';
import CommonCss from './CommonCss.vue';

addDecorator(story => ({
  components: { CommonCss },
  render(h) {
    return (
      <common-css>
        <story slot="story"></story>
      </common-css>
    )
  }
}))

// automatically import all files ending in *.stories.js
configure(require.context('../components', true, /\.stories\.js$/), module);
```

## Atoms

コンポーネントを作る際は、コンポーネントの本体となる `.vue` ファイルと、Storybookの設定である `.stories.js` を用意します。
また、Atoms/Molecules...の中にも**大量に**コンポーネントを作るため、意味のある単位でその中にディレクトリを作っておくと良いでしょう。


### スライダー

```bash
$ mkdir ./components/atoms/sliders
```

コンポーネントのファイルもスライダーだけのシンプルなものにしましょう。

```vue:./components/atoms/sliders/Slider.vue
<template>
  <input type="range" value="10" min="1" max="20" />
</template>
```

Storybookの `storiesOf()` の第一引数はStorybookの左側に表示される階層なので、`./components`と同じにしておきましょう。

```js:./components/atoms/sliders/Slider.stories.js
import { storiesOf } from '@storybook/vue';
import Slider from '@/components/atoms/sliders/Slider.vue';
 
storiesOf('atoms/sliders/Slider', module).add('default', () => ({
  components: { Slider },
  template: '<slider />'
}));
```
![storybook-structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/d8ab6c9e-a1db-e383-2148-b1a01b416f40.png)

### 切り替えボタン

```bash
$ mkdir ./components/atoms/tab
```

切り替えボタンも同様に作りましょう。BulmaのTabsを流用します。
https://bulma.io/documentation/components/tabs/#styles

ボタンの文字は仮のものを入れておきましょう。後で本当の内容に直せます。


### レビュー表示部

```bash
$ mkdir ./components/atoms/labels
$ mkdir ./components/atoms/boxes
```

レビュー表示部も同様です。こちらは

 * タイトル
 * レーティング(星5つです！)
 * 本文
 * それらを囲む枠(Box)

に分けます。
ボタンのときと同じく *内容を入れたい衝動* に駆られますが、ここは抑えて仮の文字を入れましょう。 本当の文字はもっと上のレイヤーの Pages で入れるべきです。

枠はBulmaのBoxを流用しちゃいましょう。
https://bulma.io/documentation/elements/box/

Boxの中にパーツを配置したいため、BoxのコンポーネントではSlotを用います。

```vue:./components/atoms/boxes/Box.vue
<template>
  <div class="box"><slot /></div>
</template>
```

ちなみに、このAtomsの段階ではBoxのwidthは100%と横幅いっぱいにします。実際に配置するときはCSSフレームワークのグリッドシステムなどで横幅を決めます。

![box-width-100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/84fca124-4d58-173e-fdc2-cf14aec4cd42.png)

わかりやすいように仮に文字を入れたサンプルですが、影がついているBoxの横幅がいっぱいに広がっています。

## Molecules

![molecules.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/287c826c-2022-41b2-59b9-39609673ebcb.png)


Atomsを組み合わせてMoleculesを作ります。現時点では **コンポーネントのファイルでAtomsコンポーネントを読み込む以外は同じ作りにします。**

Storybookの設定はMoleculesになっても変わりはなく、Atomsと同様にコンポーネントを指定します。

```js:./components/molecules/reviews/ReviewBox.stories.js
import { storiesOf } from '@storybook/vue'
import ReviewsBox from '~/components/molecules/reviews/ReviewsBox.vue'

storiesOf('molecules/reviews/ReviewsBox', module).add('default', () => ({
  components: { ReviewsBox },
  template: '<reviews-box />'
}))
```

### レビューの星

```bash
$ mkdir ./components/molecules/ratings
```

星を繰り返し表示できるようにします。  
この場合は `v-for` で「リストの要素に応じて繰り返す」のではなく「x回繰り返す」実装にするため、次に紹介されている `parseInt()`, `index in x` のような方法にしました。

**`v-for` において必須である `v-bind:key` が重複してしまう問題は解決できていません。**
https://stackoverflow.com/questions/44617484/vue-js-loop-via-v-for-x-times-in-a-range

星を表示する数は仮のものを用意します。

```vue:./components/molecules/ratings/Ratings.vue
<template>
  <div class="ratings">
    <span v-for="activeStar in stars" :key="activeStar">
      <rating-star />
    </span>
  </div>
</template>

<script>
import RatingStar from '@/components/atoms/ratings/RatingStar.vue'
export default {
  components: { RatingStar },
  data() {
    return {
      stars: 3
    }
  }
}
</script>

```

### スライダー

```bash
$ mkdir ./components/molecules/sliders
```

このMoleculesでは次のAtomsを読み込んで組み合わせていきます。

 * どんなことをするスライダーなのか、その名前(実際の値は後述する `props` を使う方法で入れます)
 * スライダー本体
 * スライダーの値を表示する部分

```vue:./components/molecules/sliders/DisplayAmountSlider.vue
<template>
  <div class="display-stars-panel columns">
    <div class="column is-one-fifth"><default-label /></div>
    <div class="column is-two-fifths"><slider /></div>
    <div class="column is-one-fifth"><default-label /></div>
  </div>
</template>

<script>
import DefaultLabel from '@/components/atoms/labels/DefaultLabel.vue'
import Slider from '@/components/atoms/sliders/Slider.vue'
export default {
  components: {
    DefaultLabel,
    Slider
  }
}
</script>
```

## Organisms

![organisms.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/c40fce0f-fdf1-18ca-9264-3befa11bb77f.png)

Moleculesを組み合わせてOrganismsを作ります。
こちらもStorybookの設定はMolecules, Atomsと同様にコンポーネントを読み込ませます。

### コントロールパネル(スライダー+ボタン)

```bash
$ mkdir ./components/organisms/controls
```

Moleculesと同様にこのコンポーネントで

 * スライダー部
 * 切り替えボタン

を組み合わせます。


### レビュー表示部(子コンポーネントにSlotを使った場合)

このようにSlotを設定した子コンポーネントの **タグの中に** パーツを配置すると、子コンポーネントで定義した `<slot/>` の位置に入ります。

```vue:./components/molecules/reviews/ReviewBox.vue
<template>
  <box>
    <rating-star />
    <review-title />
    <default-label />
  </box>
</template>

<style lang="scss" scoped></style>

<script>
import RatingStar from '@/components/atoms/ratings/RatingStar.vue'
import DefaultLabel from '@/components/atoms/labels/DefaultLabel.vue'
import ReviewTitle from '@/components/atoms/labels/ReviewTitle.vue'
import Box from '@/components/atoms/boxes/Box.vue'

export default {
  components: { RatingStar, DefaultLabel, ReviewTitle, Box }
}
</script>
```

## Templates

Organismsを組み合わせてTemplatesを作ります。今回は一つだけ作ります。
こちらもStorybookの設定はOrganisms, Molecules, Atomsと同様にコンポーネントを読み込ませます。

あとのPagesでデータを入れるので、この段階でまだ実際のデータを入れないようにしましょう。

![templates.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/d800d01a-4146-1fcf-1a08-01dad44905e9.png)


また、Nuxtの`layouts`と混同しそうになりますが、`layouts`は各ページで共通して表示するヘッダーを記述するだけにとどめます。
Templatesはページごとに異なる内容で、Atomsから組み上げたときに作ります。
(ヘッダーを作り込むときはAtomsから必要になるかもしれません)


## Pages

![pages.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/66946c67-6bfd-6a5f-3ad8-6869105b80fa.png)

Templatesを読み込んで(後ほど実データを投入して)Pagesを作ります。 **こちらは `components` ディレクトリではなく `pages` ディレクトリを使います。**

実際のデータを入れず、コンポーネントを組み合わせてここまで実装するとこんな感じになると思います。
(ヘッダー部分は `layouts` ディレクトリのファイルにて設定しています)

![pre-pages.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/0b31e2fa-3dc8-4c53-145c-36e6779fbdb1.png)


## 親子コンポーネント間のやり取り(props, events)

ここからは作成した各コンポーネントに実際のデータを入れていく作業になります。

図のほうがわかりやすいので、**例えばスライダーを操作したときの**データのやり取りを図にすると次の通りになります。

![props-events.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/e69154e8-0fc4-a2c5-6bdf-2d2ff232e5da.png)

スライダーをユーザーが操作した際に子コンポーネントに反映させる場合もそうですが、 **親のpagesで設定した初期値を子コンポーネントに渡すときも** `props` を使います。


### ラベルに文字を表示する: `props`

`props` を設定して、渡された値をそのまま表示できるようにします。
後述する「`props`を直接変更しないように実装する必要がある」という課題を解決するために`props`の名前は末尾に`Prop` とつけています。

```vue:./components/atoms/labels/DefaultLabel.vue
<template>
  <span>{{ displayTextProp }}</span>
</template>

<script>
export default {
  props: {
    displayTextProp: {
      type: String
    }
  }
}
</script>
```

使う側からは`v-bind`で`props`にデータを投入します。
この実装では、

 * `DefaultLabel`としてラベルを使いまわしていて
 * 「表示する数を選択」の文言は *表示する件数を制御するスライダーの* Moleculesの段階で表示できればいいので
 
 そのまま入れてしまいます。


```vue:./components/molecules/silders/DisplayAmountSlider.vue
<template>
  <div class="display-stars-panel columns">
    <!-- Moleculesの段階で固定の文言として表示したいので入れる -->
    <div class="column is-one-fifth"><default-label :display-text-prop="'表示する数を選択'"/></div>
    
    <!-- ここから下は親の設定値を動的に反映させるので後で -->
    <div class="column is-two-fifths"><slider /></div>
    <div class="column is-one-fifth"><default-label /></div> 
  </div>
</template>
```

### Storybookで「Atoms単体」として出したいときのために`props`のデフォルト値を設定する

`props`に`default`を設定すると、AtomsだけをStorybookで開いたときに「ラベルだと真っ白…」ということがなくなります。

```vue:./components/atoms/labels/DefaultLabel.vue
  props: {
    displayTextProp: {
      type: String,
      default: 'Type text here'
    }
  }
```

また、ESLintの設定によっては`props`の`default`を設定しないとwarningが出ることがあります。

> 21:5  warning  Prop 'displayAmountProp' requires default value to be set  vue/require-default-prop

ただし、ボタン(タブ)のデータとなる`props`にも`default`を設定して、これを子コンポーネント全部に入れるとなると……悲惨なことになります。  
`props`に`default`が必要なのはわかりますが…これはまだ解決できていません🙇‍♂️

```vue
 ... 
  props: {
    availableOptionsProp: {
      type: Array,
      default: function() {
        return [
          { value: 'false', text: 'A' },
          { value: 'true', text: 'B' }
        ]
      }
    }
  },
  data: function() {
    return {
      availableOptions: this.availableOptionsProp
    }
  }
```

### 子コンポーネントから動的な値を親に送る: `events` / `this.$emit()`

例えば「スライダーの値を変更したとき、その値を使って何か **フロントエンド(クライアント)側で** 処理をしたい」という場面があります。
その時には`event`にメソッドを紐づけて、メソッド内の`this.$emit()`で変更された値を親に送ります。

**子で処理しようとすると「どこで処理しているのかわからない」「コンポーネントを使いまわしたいけどイベントで勝手に動く」など混乱の原因になります。**

```vue:./components/atoms/sliders/Slider.vue
<template>
  <div>
    <input
      type="range"
      min="1"
      max="20"
      v-model="sliderValue"
      @change="onSliderChange"
    />
  </div>
</template>

<script>
export default {
  props: {
    sliderValueProp: {
      type: Number,
      default: 0
    }
  },
  data: function() {
    return {
      sliderValue: this.sliderValueProp
    }
  },
  methods: {
    onSliderChange: function() {
      this.$emit('sliderUpdate', this.sliderValue)
    }
  }
}
</script>
```

![events.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/9f4c7203-8111-bb7f-dfbc-ab38f8c8f7f4.png)



さらにイベントを親に渡したいときは **受け取ったイベントにmethodsを紐づけて、もう一度イベントを送ります。**
子コンポーネントから `this.$emit()` の第2引数で送った値は`methods`で定義したメソッドの引数で受け取ることができます。

```vue:/components/molecules/sliders/DisplayAmountSlider.vue
...
    <div class="column is-two-fifths"><slider @sliderUpdate="onSliderUpdate"/></div>
</template>

<script>
import DefaultLabel from '@/components/atoms/labels/DefaultLabel.vue'
import Slider from '@/components/atoms/sliders/Slider.vue'
export default {
  components: {
    DefaultLabel,
    Slider
  },
  props: {
    displayAmountProp: {
      type: Number,
      default: 0
    }
  },
  data: function() {
    return {
      displayAmount: this.displayAmountProp
    }
  },
  methods: {
    onSliderUpdate: function(newValue){
      this.$emit('onSliderUpdate', newValue)
    }
  }
...
```


親で処理が終わった後、そのデータを子に反映させたいときは`props`を用います。

```vue:./pages/index.vue
<template>
  <div class="root-contents">
    <main-template
      :display-amount-prop="displayAmount"
      :is-display-stars-prop="isDisplayStars"
      :display-stars-options-prop="displayStarsOptions"
      :review-data-prop="actualDisplayData"
      @onDisplaySettingsChanged="onDisplaySettingsChanged"
    />
  </div>
</template>
```

さらにpropsを子コンポーネントに渡したいときは、一度propsの値を受けてから再度propsを設定します。

```vue:./components/templates/MainTemplate.vue
<template>
  <div class="container">
    <controll-panel
      :display-amount-prop="displayAmount"
      :is-display-stars-prop="isDisplayStars"
      :display-stars-options-prop="displayStarsOptionsProp"
      @onDisplaySettingsChanged="onDisplaySettingsChanged"
    />
    ...
</template>

<script>
...
export default {
  components: {
    ControllPanel,
    ReviewsBox
  },
  props: {
    displayAmountProp: {
      type: Number,
      default: 0
    },
...
```

![props.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/c2e6eb90-1202-1fb2-8ca7-c789e3fbc9f7.png)



### `v-bind`などで`props`を直接変更させないために

スライダーの値を取得したいときに次の実装をすると問題が出ます。

 * スライダーの`value`に`props`を紐づける: 反応しない
 * スライダーの`v-bind`に`props`を紐づける: 反応するが **warningが出る**

 > vue.esm.js:628 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "sliderValueProp"
 
そのため「`v-model`に紐づけて変更する用」に`data`を用意しないといけません。
コンポーネントの`data`なのか`props`なのかを区別する必要があるため、`props`としている変数は末尾に`Props`とつけています。

このように`data`も設定すると ~~より子コンポーネントで状態を保持している感が強まるので~~ 混乱しないためにも

 * 親コンポーネントに変更後の値を渡して
 * 親で通信などの処理をしてから

その結果を子に反映させる実装をします。


### 親から`props`を受け取りたい場合は`data`ではなく`computed`にしないと反映されない

…と言いたいのですが、`props`の値を`data`で受ける実装にしてしまうと、親からデータが渡ってきたときに **それが反映されません。**

```vue:反映されない例
<script>
export default {
  ...
  props: {
    displayAmountProp: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      displayAmount: this.displayAmountProp
    }
  },
  ...
```

この場合 `data` の代わりに `computed` を使います。

```vue:反映される例
<script>
export default {
  ...
  props: {
    displayAmountProp: {
      type: Number,
      default: 0
    }
  },
  computed: {
    displayAmount() {
      return this.displayAmountProp
    }
  },
  ...
```

`props`/`events`でコンポーネント間のデータのやり取りをしていて一番ハマったポイントです。
スライダーなどユーザーの操作で変更される可能性がある`props`は`computed`と併用しましょう。

## 動作確認

組みあがったものの動作を確認したいときは次のコマンドを

```bash
$ yarn dev
```

StorybookでUIパーツを確認したいときは次のコマンドを使います(今頃かよ、という気がしますが)

```bash
$ yarn storybook
```

*Windowsの場合は `C:\Windows\system32` を環境変数に追加しないと、ブラウザを開けずにStorybookが終了します。 
https://qiita.com/maedadada/items/97c54b68d5825b60c393

![storybook.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/85b79eb8-f6eb-842e-9c4c-3ceaf4260798.png)


## おわりに

Atomic Designを実践し、WebアプリのUIをコンポーネントにすることで「この機能増やしてー」と頼まれたときにAtoms/Molecules/Organismsを流用して変更を素早く実現できています。

**今のところは💦**

ただし、その一方で

 * Atoms/Molecules/Organismsの3つにUIパーツが全て収まらない
 * Atomsを用いたOrganismsが出てきてしまう、など悩ましい状況が出てくる
 * コンポーネントを大量に作成したり、大量のpropsやeventsを設定する、となると時間がかかる  
 ~~そしてAdvent Calendarに間に合わなくなる🙇‍♂️~~

といった問題があります。
適切にAtomsなどに分割して仕様変更に対応しやすい構造を目指しましょう。

(後ほど使いまわしたい共用部分が発生して)手戻りを許容するのであれば、Atomsを作成して`props`/ `events`を設定するコストを減らすためにMoleculesから作成するのもアリだと考えています。

~~Advent Calendar遅れてホント申し訳ない~~

最後に、このサンプルのソースコードはこちらです。
https://github.com/ysd-marrrr/nuxt-atomic-design-20191205

＼次は @miyatay さんです／


## 参考

Atomic Design by Brad Frost
http://atomicdesign.bradfrost.com/

Atomic Design を分かったつもりになる | DeNA DESIGN BLOG
https://design.dena.com/design/atomic-design-%E3%82%92%E5%88%86%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%A4%E3%82%82%E3%82%8A%E3%81%AB%E3%81%AA%E3%82%8B/

Vue.js × Atomic Design - コンポーネント分割の指針 / Vue.js and Atomic Design - Guideline for components division
https://speakerdeck.com/nrslib/vue-dot-js-and-atomic-design-guideline-for-components-division

Nuxt.jsでStorybookを使用してみたメモ - WebTecNote
https://tenderfeel.xsrv.jp/javascript/4121/

Nuxt.jsへのStorybookの導入と、Sassの変数や共通CSSを読めるようにする設定 - tacamy--blog
http://tacamy.hatenablog.com/entry/2019/05/27/113131

Nuxt.js + Vuetifyのセットアップでcore-js関係の依存関係が見つからないと怒られた
https://qiita.com/auau3700/items/27bd33ee8df6d3e505f4

Vue Js - Loop via v-for X times (in a range)
https://stackoverflow.com/questions/44617484/vue-js-loop-via-v-for-x-times-in-a-range

ng s -oコマンド実行時の「spawn cmd ENOENT」エラー対処法
https://qiita.com/maedadada/items/97c54b68d5825b60c393