はじめに
Vue / Nuxt で form 周りを触っていると、双方向バインディングのありがたさが身に染みるこの頃ですが、Atomic デザインをしていると Atom / Molecules / Organisms / templates / pages にコンポーネントが徹底的に分解されていくので、 親子間でのデータの受け渡しが必須になってくるかと思います。
するとです、multiple タイプな select でどうデータを受け渡せば良いのかで詰まりました。😕
ですので、自戒もこめて下記にその方法を残しておきます。
TL; DR
以下は、オレオレ Atomic デザインで親子間を構築してますが、components をどう分割しているかだけなので、そのまま自身の環境に合わせればたぶん動くと思います。
親コンポーネント
1<template>
2 <SelectMultiple
3 :options="options"
4 :selected="selected"
5 @select="$emit('select', $event)"
6 />
7</template>
8
9<script>
10import SelectMultiple from '@/components/Atoms/SelectMultiple';
11
12export default {
13 components: {
14 SelectMultiple
15 },
16 props: {
17 selected: {
18 type: Array,
19 required: true
20 }
21 },
22 data: () => ({
23 options: [
24 { text: 'hoge1', value: 'hoge1' },
25 { text: 'hoge2', value: 'hoge2' }
26 ]
27 })
28</script>
子コンポーネント
1<template>
2 <select
3 v-model="select"
4 multiple
5 >
6 <option
7 v-for="(option, key) in options"
8 :key="key"
9 :value="option.value"
10 >
11 {{ option.text }}
12 </option>
13 </select>
14</template>
15
16<script>
17export default {
18 props: {
19 selected: {
20 type: Array,
21 required: true
22 },
23 options: {
24 type: Array,
25 required: true
26 }
27 },
28 computed: {
29 select: {
30 get () {
31 return this.$props.selected;
32 },
33 set (value) {
34 this.$emit('select', value);
35 }
36 }
37 }
38};
39</script>
通常の親子間の受け渡し
props と $emit
親子間でデータを受け渡す際は、
- 親コンポーネント -> 子コンポーネント : props オプション
- 子コンポーネント -> 親コンポーネント : $emit メソッド
ここで注意したいのが、props で渡された値は更新ができない readonly であることです。
v-model は シンタックスシュガー
v-model は、v-bind:value(各種入力値)と v-on:input(各種イベント)のシンタックスシュガー(糖衣構文)で、簡潔に記述するために v-model が多用されています。
ところが、親子間のデータ受け渡しには v-model ではなく、v-bind:value と v-on:input を使う方がスムーズに動きます。 これは、前述した props が読み込み専用であることに起因しています。
v-model に props で渡されたデータを入力するとエラーがおきます。 これは、読み込み専用であるデータを v-model 内で書き換えようとするために起きるエラーです。
そこで、これを回避するために、v-model を分解して、props のデータを v-bind:value に入力、v-on:input で発火したイベント処理を $emit で親に渡して、props を更新しないようにしてあげます。
1<template>
2 <div>
3 <input v-model="value"> // ×
4 <input v-bind:value="value" v-on:input="$emit('input', $event.target.value)"> // ◯
5 </div>
6</template>
7
8<script>
9export default {
10 props: [value]
11}
12</script>
ちなみに、各入力タグの v-model のシンタックスシュガーは下記のようになっています。
入力タグ | シンタックシュガー |
---|---|
input[text], textarea | v-model = v-bind:value + v-on:input |
input[check], input[radio] | v-model = v-bind:checked + v-on:change |
select | v-model = v-bind:value + v-on:change |
ところが multiple な select では、、、
multiple タイプの select では複数の option の value が選択されて、それを親に渡す必要があります。 ところが、前述の select のシンタックスシュガーと $emit では、はじめに選択された option の value のみ渡されるような挙動になりました。
1// ×
2<template>
3 <select
4 :value="selected"
5 :change="$emit('select', $event.target.value)"
6 multiple
7 >
8以下略
これは、$event.target.value
が原因です。
$event
は、Javascript におけるイベント発生時のオブジェクトが詰まっているのですが、
$event.target.value
では、はじめに選択されたオプションの value しか見れないので、結果的に複数ではなく、1つだけ渡された格好になりました。
そこで、$event
のあらゆるプロパティを覗いて複数の value を渡そうとしたのでしたが、結果的に上手くはいきませんでした。(もしかしたら、何らかのプロパティを渡せば動くかも?)
そこで敢えて v-model を使うことに
この方法は、こちらの方の記事を参考に multiple な select に適用したものです。
v-model に直接 props を入力するのではなく、computed
を介して、v-model の get
、set
メソッドでデータの受け渡しを制御する方法です。
こうすることによって、複数の option の value が親に渡せることが確認できました。
1<template>
2 <select
3 v-model="select"
4 multiple
5 >
6 <!-- 略 -->
7 </select>
8</template>
9
10<script>
11export default {
12 props: {
13 selected: {
14 type: Array,
15 required: true
16 }
17 },
18 computed: {
19 select: {
20 get () {
21 return this.$props.selected;
22 },
23 set (value) {
24 this.$emit('select', value);
25 }
26 }
27 }
28};
29</script>
おわりに
ざっと、現在の私の知見としては下記のような感じです。
- 同一コンポーネント間であれば、v-model でさくっとデータの受け渡しが可能
- 親子コンポーネント間であれば、
v-bind:(props) + v-on:(event)
でデータの受け渡しが可能 - ただし、multiple な select のように、幾つかの入力タグでは、適切な
$event.target.value
が見出せないので、v-model の get、set で親子間のデータ渡しを制御すること
うーん、難しいな 😓
やはり説明書を読まない、ドキュメントを読めていないツケが回ってきているような状態ですね。