【Vue】モーダルの実装を通して学ぶ.sync修飾子(ついでにv-model復習)【Gif多数】

# さいしょに
Vue 2.3.0から新たに追加された`.sync`修飾子、使いこなせているでしょうか？　この記事は`sync`をイマイチ使いこなせていない自分向けの勉強録です。
　`.sync`を使うと、「*子要素で親要素から受け取った値を変更できる*」ような感覚が得られます(ただし、実際は子要素から親要素にイベントを渡しているだけです)。
　以下のような検索モーダルを実装を通して、v-modelの復習をしながら、.sync修飾子を学びます。さらにはv-modelと.syncの違いについても学べるという寸法です。

![2020-05-05 01.14.51.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/c13f7a54-5e00-1c0c-3fd7-61e127fba01d.gif)


※ご注意。この記事では主役である.syncが登場するのは一番最後です。
# 対象者
- Vueチョットワカル
- v-modelが何となく使える
- `.sync`の使い方がいまいちわからない
- 「何となく使える」ではなく、原理も知りたい
- 実際に手を動かしながら学べる人(スマホでサラッと読むだけだと分かりづらいと思います)

# 実行環境
- Vue CLI 4.3.1
- Node 12.16.1
- npm 6.14.4
- Vue 2.6.11

Vueが`2.3.0`以上であれば、他は多少バージョンが違っても問題ないです。

# まずは環境を整えましょう
Vue CLIを使ってプロジェクトを作成しましょう。

```shell:ターミナル
npm i -g @vue/cli # Vue CLIの導入がまだの方
vue create vue-sync # プロジェクトを作成
Please pick a preset: default (babel, eslint) # 聞かれたらエンターキーを押しましょう
cd vue-sync
```
cssを書いてモーダルを実装するのは面倒くさいので、世界で最も人気のあるVue.jsフレームワーク([本人談](https://vuetifyjs.com/ja/getting-started/quick-start/#%E3%82%AF%E3%82%A4%E3%83%83%E3%82%AF%E3%82%B9%E3%82%BF%E3%83%BC%E3%83%88))である`Vuetify`を導入しましょう。

```shell:ターミナル
vue add vuetify
? Choose a preset: Default (recommended)
```

# 本編
## 一旦、起動して動作確認

```shell:ターミナル
npm run serve
# http://localhost:8080/にアクセスしましょう
```
以下の画面が表示されれば成功です。
![スクリーンショット 2020-05-04 21.13.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/98aa9662-ec92-c923-f622-696f67ba9d08.png)

## まずは、いつものv-modelを
`App.vue`を書き換えて、いつもの「テキストフィールドに入力した内容がリアルタイムで他の要素に反映されるやつ」を実装しましょう。


![2020-05-04 21.26.37.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/9e5d2892-1952-6fc8-8a6a-b99c7c7ab1e5.gif)

```vue:src/App.vue
<template>
  <v-app>
    <v-content>
      <v-container>
        <v-text-field label="検索キーワード" v-model="keyword" />
        <p>検索キーワード:{{keyword}}</p>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
export default {
  data: () => ({
    keyword: ''
  })
}
</script>
```

## テキストフィールドをモーダル内に移動
まずはコンポーネント分割は何も考えずにモーダルを実装していきます。
![2020-05-04 21.43.34.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/535455f4-afe4-4e22-766f-1bd42207cd1d.gif)


```vue:src/App.vue
<template>
  <v-app>
    <v-content>
      <v-container>
        <v-dialog v-model="isModalOpen">
          <v-card>
            <v-card-text>
              <v-text-field label="検索キーワード" v-model="keyword" />
            </v-card-text>
            <v-card-actions>
              <v-btn @click="isModalOpen = false">OK</v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>
        <v-btn @click="isModalOpen = true">検索モーダルを開く</v-btn>
        <p>検索キーワード:{{keyword}}</p>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
export default {
  data: () => ({
    keyword: '',
    isModalOpen: false
  })
}
</script>
```

## 検索モーダルを子コンポーネントに移す
検索モーダルを他のページでも使いまわしたいという要望があがったとしましょう。要望の詳細は以下のとおりです。

- モーダルの開閉を制御する変数(isModalOpen)は親コンポーネント側で保持する。
- モーダルを開くボタンは親コンポーネント側においておく。

先ほど書いたコードの`v-dialog`部分を`SearchModal`子コンポーネントに移す必要があります。とりあえず`src/components/SearchModal.vue`を作成して、v-dialog部分をコピペします。そのままでは、親要素から子要素であるSearchModalコンポーネントに値が伝わらないので`isModalOpen`と`keyword`をpropsとして渡すようにします。

```vue:src/App.vue
<template>
  <v-app>
    <v-content>
      <v-container>
        <search-modal :isModalOpen="isModalOpen" :keyword="keyword" />
        <v-btn @click="isModalOpen = true">検索モーダルを開く</v-btn>
        <p>検索キーワード:{{keyword}}</p>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
import SearchModal from '@/components/SearchModal'

export default {
  components: { SearchModal },
  data: () => ({
    keyword: '',
    isModalOpen: false
  })
}
</script>
```

```vue:src/components/SearchModal.vue
<template>
  <v-dialog v-model="isModalOpen">
    <v-card>
      <v-card-text>
        <v-text-field label="検索キーワード" v-model="keyword" />
      </v-card-text>
      <v-card-actions>
        <v-btn @click="isModalOpen = false">OK</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>


<script>
export default {
  name: 'SearchModal',
  props: {
    isModalOpen: { type: Boolean, default: false },
    keyword: { type: String, default: '' }
  }
}
</script>
```
上記コードの問題点にお気づきでしょうか？　動作確認をしてみると、モーダルを閉じたときにコンソールエラーがでてしまいます。
![2020-05-04 21.55.57.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/22521bb8-c82b-9a1c-954e-26e3783fc6a5.gif)
問題点は`src/components/SearchModal.vue`にあります。v-dialogに設定した`v-model="isModalOpen"`の部分です。これでは、親コンポーネントから渡されたisModalOpenを書き換えようとしてしまいます。また、モーダル内のOKボタンの`@click="isModalOpen = false"`でも親から渡されたpropを変更しようとしています。修正していきましょう

## propを直接変更しないように修正
モーダルの外側がクリックされたとき、及びOKボタンがクリックされたときには親コンポーネントに`close`イベントを渡すようにしましょう。俗に言う`event up`というやつです。また、検索キーワードが変更されたときは親コンポーネントに`input`イベントを渡すようにしましょう。

```vue:src/App.vue
<template>
  <v-app>
    <v-content>
      <v-container>
        <search-modal
          :isModalOpen="isModalOpen"
          :keyword="keyword"
          @input="keyword = $event"
          @close="isModalOpen = false"
        />
        <v-btn @click="isModalOpen = true">検索モーダルを開く</v-btn>
        <p>検索キーワード:{{keyword}}</p>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
import SearchModal from '@/components/SearchModal'

export default {
  components: { SearchModal },
  data: () => ({
    keyword: '',
    isModalOpen: false
  })
}
</script>
```

```vue:src/components/SearchModal.vue
<template>
  <v-dialog :value="isModalOpen" @click:outside="$emit('close')">
    <v-card>
      <v-card-text>
        <v-text-field
          label="検索キーワード"
          :value="keyword"
          @input="$emit('input', $event)"
        />
      </v-card-text>
      <v-card-actions>
        <v-btn @click="$emit('close')">OK</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>


<script>
export default {
  name: 'SearchModal',
  props: {
    isModalOpen: { type: Boolean, default: false },
    keyword: { type: String, default: '' }
  }
}
</script>
```

## v-modelを使う
親コンポーネント(`src/App.vue`)の↓この部分に着目しましょう。

```vue:src/App.vue
<search-modal
  isModalOpen="isModalOpen"
  keyword="keyword"
  input="keyword = $event"
  @close="isModalOpen = false"
/>
```
これは次のように書き換えることができますね。

```diff:src/App.vue
<search-modal
  isModalOpen="isModalOpen"
- keyword="keyword"
- input="keyword = $event"
+ v-model="keyword"
  @close="isModalOpen = false"
/>
```
なぜなら、`v-model="keyword"`は`keyword="keyword" input="keyword = $event"`の略だからです。
ここまでで、`v-model`の復習ができたはずです。

## 検索モーダルに要望追加が…
ここで、検索モーダル内に「大文字・小文字を区別する」というチェックボックスを追加してほしいという要望が来ました。また検索処理(APIとの通信部分)は親コンポーネント(`src/App.vue`)に書きたいので、チェック状態は親コンポーネントで管理する必要があるそうです。これまで通り`event up`の考えを使うと、以下のような変更で対処できそうです。

- 子コンポーネント
  - propsに`considerCase`を追加(trueなら大文字と小文字を区別する)
  - チェックボックスを追加し、チェックされたたら、親に`toggle`イベントを伝える。
- 親コンポーネント
  - dataに`considerCase`を追加
  - `toggle`イベントを受け取り、`considerCase`のbool値を反転させる。

ソースコードは以下のようになります。長くなるので途中を省略しています。

```vue:src/App
<template>
  <!-- 略 -->
        <search-modal
          <!-- 略 -->
          :considerCase="considerCase"
          @toggle="considerCase = !considerCase"
        />
        <v-btn @click="isModalOpen = true">検索モーダルを開く</v-btn>
        <p>検索キーワード:{{keyword}}</p>
        <!-- ↓追加 -->
        <p>大文字・小文字を区別する:{{considerCase}}</p>
  <!-- 略 -->
</template>

<script>
import SearchModal from '@/components/SearchModal'

export default {
  components: { SearchModal },
  data: () => ({
    keyword: '',
    isModalOpen: false,
    considerCase: false // 追加
  })
}
</script>
```

```vue:src/components/SearchModal.vue
<template>
  <!-- 略 -->
        <v-checkbox
          label="大文字・小文字を区別する"
          :value="considerCase"
          @click="$emit('toggle')"
        />
  <!-- 略 -->
</template>


<script>
export default {
  name: 'SearchModal',
  props: {
    isModalOpen: { type: Boolean, default: false },
    considerCase: { type: Boolean, default: false }, // 追加
    keyword: { type: String, default: '' }
  }
}
</script>
```
やりました！　これでチェックボックスが追加され、親要素のdataも正しく書き換わっているようです。
![2020-05-04 23.52.27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/c13d42a8-be14-b4ab-a433-4318df96e4ab.gif)

ちょっとまってください。`src/App.vue`のソースコードの以下の部分を見てください。

```vue:src/App.vue
<search-modal
  :isModalOpen="isModalOpen"
  v-model="keyword"
  @close="isModalOpen = false"
  :considerCase="considerCase"
  @toggle="considerCase = !considerCase"
/>
```
呼び出しているコンポーネントはテキストフィールドとチェックボックスとボタンしかないのにやたら引数が多いです。なんとかならないか…。そんなときに出番となるのが`.sync`修飾子です。

## sync修飾子の登場に備えて
理由は後でわかるので、騙されたと思ってとりあえず、以下のように子要素のclose, toggleイベント名を変更しましょう。

- closeイベント→update:isModalOpenイベント
- toggleイベント→update:considerCaseイベント

また、$emitの第2引数にイベント発火後の変数の値を指定します。

```vue:src/components/SearchModal.vue
<template>
  <v-dialog :value="isModalOpen" @click:outside="$emit('update:isModalOpen', false)">
    <v-card>
      <v-card-text>
        <v-text-field
          label="検索キーワード"
          :value="keyword"
          @input="$emit('input', $event || false)"
        />
        <v-checkbox
          label="大文字・小文字を区別する"
          :value="considerCase"
          @change="$emit('update:considerCase', $event || false)"
        />
      </v-card-text>
      <v-card-actions>
        <v-btn @click="$emit('update:isModalOpen', false)">OK</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>
```
v-checkboxのここが少し難しいかもしれません。

```vue
  @change="$emit('update:considerCase', $event || false)"
```
v-checkboxではチェックボックスがクリックされると、`change`イベントが発火します。チェックがオンになった場合は$event変数に`true`が入り、チェックがオフになったときは$eventに`null`が入ります。今回はチェックがオフになった場合はfalseを返したいので、`$event || false`とします。
<details><summary>if文じゃないのに`||`を使っていて困惑している人向け</summary><div>

||の左がnull, false, undefinedなどfalsyの場合は右側の値が返されます。

```js:例
console.log(null || '右')
// => 右
```
</div>
</details>

子コンポーネントを書き換えたので、それに合わせて親コンポーネントの呼び出し部分も書き換えていきます。

```vue:src/App.vue
<search-modal
  :isModalOpen="isModalOpen"
  @update:isModalOpen="isModalOpen = $event"
  v-model="keyword"
  :considerCase="considerCase"
  @update:considerCase="considerCase = $event"
/>
```

なんだか、同じ変数名が繰り返されていますね。実はこれを`.sync`を使って書き換えることができるのです。

```diff:src/App.vue
-  :isModalOpen="isModalOpen"
-  @update:isModalOpen="isModalOpen = $event"
+  :isModalOpen.sync="isModalOpen"

-  :considerCase="considerCase"
-  @update:considerCase="considerCase = $event"
+  :considerCase.sync="considerCase"
```
とてもスマートになりました。

これで.syncの解説は終わりです。
.sync修飾子を使うときはいちいち理論を考えずに以下の感覚があると良いです。

- 親要素：子要素で変更させたい変数に対して`:変数.sync`とつける
- 子要素では`$emit('update:変数名', 値)`で変更できる
- (ただし、頭の片隅では、「子要素で値を変更しているわけではなく、親要素にイベントを通知している」と覚えておく)

## 課題
この課題を解くとv-modelと.syncの違いがわかり、使い分けができるようになります(決して解説が面倒になったわけではない。)。

またまた追加要望です。現在、モーダル内に検索キーワード欄があると思います。その下に「除外キーワード」入力欄を追加してほしいと要望がありました。

イメージ![2020-05-05 01.17.07.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297745/1fd56ec6-855e-81b9-e231-37fd80968ff7.gif)


課題１. 除外キーワードを保持しておく変数`excludeKeyword`を親コンポーネントのデータに追加してください。
課題2. 子コンポーネントに除外キーワードテキストフィールドを追加してください。
課題3. 親コンポーネントから子コンポーネントに、`excludeKeyword`を渡し、また子コンポーネントでの除外キーワードの入力が親に伝わるようにしてください。ただし、v-modelはすでにkeywordに使われてしまっているので、`:excludeKeyword.sync`を使うようにしましょう

# 参考文献
- [Vuetify](https://vuetifyjs.com/en/)
- [公式ドキュメント](https://jp.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A3%BE%E5%AD%90)