さいしょに
Vue 2.3.0から新たに追加された.sync
修飾子、使いこなせているでしょうか? この記事はsync
をイマイチ使いこなせていない自分向けの勉強録です。
.sync
を使うと、「子要素で親要素から受け取った値を変更できる」ような感覚が得られます(ただし、実際は子要素から親要素にイベントを渡しているだけです)。
以下のような検索モーダルを実装を通して、v-modelの復習をしながら、.sync修飾子を学びます。さらにはv-modelと.syncの違いについても学べるという寸法です。
※ご注意。この記事では主役である.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を使ってプロジェクトを作成しましょう。
npm i -g @vue/cli # Vue CLIの導入がまだの方
vue create vue-sync # プロジェクトを作成
Please pick a preset: default (babel, eslint) # 聞かれたらエンターキーを押しましょう
cd vue-sync
cssを書いてモーダルを実装するのは面倒くさいので、世界で最も人気のあるVue.jsフレームワーク(本人談)であるVuetify
を導入しましょう。
vue add vuetify
? Choose a preset: Default (recommended)
本編
一旦、起動して動作確認
npm run serve
# http://localhost:8080/にアクセスしましょう
まずは、いつものv-modelを
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>
テキストフィールドをモーダル内に移動
まずはコンポーネント分割は何も考えずにモーダルを実装していきます。
<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として渡すようにします。
<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>
<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>
上記コードの問題点にお気づきでしょうか? 動作確認をしてみると、モーダルを閉じたときにコンソールエラーがでてしまいます。
問題点はsrc/components/SearchModal.vue
にあります。v-dialogに設定したv-model="isModalOpen"
の部分です。これでは、親コンポーネントから渡されたisModalOpenを書き換えようとしてしまいます。また、モーダル内のOKボタンの@click="isModalOpen = false"
でも親から渡されたpropを変更しようとしています。修正していきましょう
propを直接変更しないように修正
モーダルの外側がクリックされたとき、及びOKボタンがクリックされたときには親コンポーネントにclose
イベントを渡すようにしましょう。俗に言うevent up
というやつです。また、検索キーワードが変更されたときは親コンポーネントにinput
イベントを渡すようにしましょう。
<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>
<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
)の↓この部分に着目しましょう。
<search-modal
isModalOpen="isModalOpen"
keyword="keyword"
input="keyword = $event"
@close="isModalOpen = false"
/>
これは次のように書き換えることができますね。
<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
イベントを伝える。
- propsに
- 親コンポーネント
- dataに
considerCase
を追加 -
toggle
イベントを受け取り、considerCase
のbool値を反転させる。
- dataに
ソースコードは以下のようになります。長くなるので途中を省略しています。
<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>
<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も正しく書き換わっているようです。
ちょっとまってください。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引数にイベント発火後の変数の値を指定します。
<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のここが少し難しいかもしれません。
@change="$emit('update:considerCase', $event || false)"
v-checkboxではチェックボックスがクリックされると、change
イベントが発火します。チェックがオンになった場合は$event変数にtrue
が入り、チェックがオフになったときは$eventにnull
が入ります。今回はチェックがオフになった場合はfalseを返したいので、$event || false
とします。
if文じゃないのに`||`を使っていて困惑している人向け
||の左がnull, false, undefinedなどfalsyの場合は右側の値が返されます。
console.log(null || '右')
// => 右
子コンポーネントを書き換えたので、それに合わせて親コンポーネントの呼び出し部分も書き換えていきます。
<search-modal
:isModalOpen="isModalOpen"
@update:isModalOpen="isModalOpen = $event"
v-model="keyword"
:considerCase="considerCase"
@update:considerCase="considerCase = $event"
/>
なんだか、同じ変数名が繰り返されていますね。実はこれを.sync
を使って書き換えることができるのです。
- :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の違いがわかり、使い分けができるようになります(決して解説が面倒になったわけではない。)。
またまた追加要望です。現在、モーダル内に検索キーワード欄があると思います。その下に「除外キーワード」入力欄を追加してほしいと要望がありました。
課題1. 除外キーワードを保持しておく変数excludeKeyword
を親コンポーネントのデータに追加してください。
課題2. 子コンポーネントに除外キーワードテキストフィールドを追加してください。
課題3. 親コンポーネントから子コンポーネントに、excludeKeyword
を渡し、また子コンポーネントでの除外キーワードの入力が親に伝わるようにしてください。ただし、v-modelはすでにkeywordに使われてしまっているので、:excludeKeyword.sync
を使うようにしましょう