Vue.jsでForm要素のラッパーコンポーネントを作る際の2つの方法（v-bind.sync, v-model）

input欄を含むコンポーネントを作る際に毎回忘れるのでメモ

# v-bind.syncを使う方法

https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier

`v-bind.sync`は`v-bind:hoge`, `v-on:update:hoge="hoge = $event"`のシンタックスシュガーです。
これを意識して子コンポーネントでイベントを発行することで、Form要素を内包するコンポーネントを簡潔に実装できます。

```vue
<!-- <MyInput v-bind:hoge.sync="hoge">は以下と同義 -->
<MyInput
  :hoge="hoge" 
  v-on:update:hoge="hoge = $event"
>
```

### プリミティブな値の場合
input要素単体のコンポーネントなどの場合は、単純にコンポーネント内での変更感知のemit名を`update:props名`として値を発行すればOKです。

#### 子コンポーネント（input要素）

```MyInput.vue
<template>
  <label
    >{{ label }}
    <input type="text" :value="value" @input="updateValue($event.target.value)" />
  </label>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  props: {
    label: {
      require: true,
      type: String
    },
    value: {
      require: true,
      type: String
    }
  },
  setup(_, { emit }) {
    const updateValue = (value: string) => emit("update:value", value);
    return {
      updateValue
    };
  }
});
</script>
```

#### 親コンポーネント

```Parent.vue
<MyInput 
  label="ラベル" 
  :value.sync="myInputValue" 
/>
```

### オブジェクトの場合

Form要素を一括でコンポーネントにする場合などは、子コンポーネントでpropsを個々に定義しつつ
`v-bind.sync`に直接オブジェクトを渡すことで簡潔に実装できます。

#### 子コンポーネント

```MyForm.vue
<template>
  <form>
    <label>
      title
      <input
        type="text"
        :value="title"
        @input="updateValue('title', $event.target.value)"
      />
    </label>
    <label>
      content
      <input
        type="textarea"
        :value="content"
        @input="updateValue('content', $event.target.value)"
      />
    </label>
    <button @click="onSubmit">submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  props: {
    title: {
      require: true,
      type: String
    },
    content: {
      require: true,
      type: String
    }
  },
  setup(props, { emit }) {
    const updateValue = (key: string, value: string) => {
      emit(`update:${key}`, value);
    };
    const onSubmit = () => emit("submit");
    return {
      onSubmit,
      updateValue
    };
  }
});
</script>
```

#### 親コンポーネント

```Parent.vue
<MyFormSync 
  v-bind.sync="myFormData" 
  @submit="onSubmit"
/>
```



# v-modelを使う方法

https://vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model

`v-model`は`v-bind:value="hoge"`, `@input="hoge = $event.target.value"`のシンタックスシュガーです。

```vue
<!-- <input v-model=hoge>は以下と同義 -->
<MyInput
  :value="name" 
  @input="name = $event.target.value"
>
```

### プリミティブな値の場合
v-modelをカスタムコンポーネントで使う場合は、子コンポーネントが受け取るpropsを必ず`value`にする必要があります。また、$emitの際のイベント名は必ず`input`にする必要があります。

#### 子コンポーネント

```MyInput.vue
<template>
  <label
    >{{ label }}
    <input type="text" :value="value" @input="updateValue($event.target.value)" />
  </label>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  props: {
    label: {
      require: true,
      type: String
    },
    value: {
      require: true,
      type: String
    }
  },
  setup(_, { emit }) {
    const updateValue = (value: string) => emit("input", value);
    return {
      updateValue
    };
  }
});
</script>
```

#### 親コンポーネント

```Parent.vue
<MyInput 
  label="ラベル" 
  v-model="myInputValue"
/>
```

### オブジェクトの場合

Form要素をまとめて一つのコンポーネントにする場合はオブジェクトを渡せると良いですよね。
その際は、プリミティブの値と同等の制約を持ちつつ、emitの際に、`emit("input", { ...props.value, [key]: value });`の形式で変更プロパティと、他のプロパティのマージを行い、コピーのオブジェクトをemitするようにします。


#### 子コンポーネント（form要素）

```MyForm.vue
<template>
  <form>
    <label>
      title
      <input
        type="text"
        :value="value.title"
        @input="updateValue('title', $event.target.value)"
      />
    </label>
    <label>
      content
      <input
        type="textarea"
        :value="value.content"
        @input="updateValue('content', $event.target.value)"
      />
    </label>
    <button @click="onSubmit">submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

type FormData = {
  title: string;
  content: string;
};

export default defineComponent({
  props: {
    value: {
      require: true,
      type: Object as () => FormData
    }
  },
  setup(props, { emit }) {
    const updateValue = (key: string, value: string) => {
      emit("input", { ...props.value, [key]: value });
    };
    const onSubmit = () => emit("submit");
    return {
      onSubmit,
      updateValue
    };
  }
});
</script>

```

#### 親コンポーネント

```Parent.vue
<MyForm 
  v-model="myFormValue" 
  @submit="onSubmit"
/>
```

# どっちを使うべき？
完全に好みだと思いますが、同様の目的であれば`v-bind.sync`の方がprops名の制約や、オブジェクト要素の場合の値のマージなどが必要ないので、使いやすいかなと思ってます。

# 参考

以下記事参考にさせて頂きました!!良記事ありがとうございます。

- [Vue.js: Using v-model with objects for custom components - Simon Kollross](https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components)
- [Vue.jsの双方向バインディング再入門 - Qiita](https://qiita.com/fruitriin/items/dc75af413da3661f9e78)
- [Vue.js：v-modelと$emitを使ってデータを読み書きする子コンポーネントをつくる (2019/10/13追記) - Qiita](https://qiita.com/ozone/items/b75efe5c449cbc469b1e)