[Vue3 CompositionAPI]カンマ区切りのinputコンポーネントを作成する

## はじめに
この記事では、Vue3系でカンマ区切りで表示するinputコンポーネントを作成します。
### 使用技術
- Vue3
- CompositionAPI

## 作成するinputコンポーネントの要件
1. 数値の入力のみを許容する
1. コンポーネント利用側は値を数値型(null含む)として入力値を受け取れる
1. blur時は`1,234`のようにカンマ区切りで値を表示する
1. focus時は'1234'のように数値として値を表示する
1. 以下のように`v-model`ディレクティブを使用できる
```
<CommaInput v-model="hoge" />
```

## 成果物
CodeSandBoxでソースコードを書きました。
こちらで主題のinputコンポーネントの挙動を確認してみてください！
[CodeSandBox上で書いたコード](https://codesandbox.io/embed/nifty-tdd-l1dwmq?fontsize=14&hidenavigation=1&theme=dark)

## 実装
### 主題のinputコンポーネント
```vue:CommaInput.vue
<template>
  <input
    :value="commaSeparetedValue"
    v-on:focus="onFocus"
    v-on:blur="onBlur"
    @input="onInput"
  />
</template>

<script>
import { ref, computed, defineComponent } from "vue";

export default defineComponent({
  props: {
    modelValue: {
      required: true,
    },
  },
  setup(props, context) {
    const isFocus = ref(false);

    const commaSeparetedValue = computed(() => {
      if (props.modelValue === null) {
        return "";
      }

      return isFocus.value
        ? props.modelValue
        : Number(props.modelValue).toLocaleString();
    });

    const onFocus = () => {
      isFocus.value = true;
    };

    const onBlur = () => {
      isFocus.value = false;
    };

    const onInput = (event) => {
      const value = event.target.value;
      if (isNaN(value)) {
        return;
      }
      context.emit("update:modelValue", value);
    };

    return {
      commaSeparetedValue,
      onFocus,
      onBlur,
      onInput,
    };
  },
});
</script>

<style scoped>
input {
  text-align: right;
}
</style>
```

### 呼び出し側
```vue:App.vue
<template>
  <p>カンマ区切りのInput</p>
  <CommaInput v-model="inputItem" />
</template>

<script>
import { ref } from "vue";
import CommaInput from "./components/CommaInput.vue";
export default {
  name: "App",
  components: {
    CommaInput,
  },
  setup() {
    const inputItem = ref(null);

    return {
      inputItem,
    };
  },
};
</script>
```

## ソースコード解説 (CommaInput.vue について)
### template
```vue
<template>
  <input
    :value="commaSeparetedValue"
    v-on:focus="onFocus"
    v-on:blur="onBlur"
    @input="onInput"
  />
</template>
```
主題のコンポーネントは、`focus`, `blur`, `input`のイベントを制御する必要があるため、それぞれのイベントの受け取りを設定します。
表示する値については、カンマ区切りの文字列として表示する場合(blur時)と、通常の数値を表示する場合(focus時)があるため、`computed`な値をバインドする必要があります。
そのため、`:value(v-bind:valueの省略記法)`を用い、のちに解説する`commaSeparetedValue`をバインドします。

### script
#### propsについて
```vue
<script>
import { ref, computed, defineComponent } from "vue";

export default defineComponent({
  props: {
    modelValue: {
      required: true,
    },
  },
~省略~
});
</script>
```
主題のコンポーネントは、使用側が`v-model`ディレクティブで入力値をセットできる必要があるため、propsには`v-model="hoge"`として使用するための予約語である`modelValue`を宣言します。
今回は`v-model="hoge"`として使用するために`modelValue`としましたが、`v-model:hoge`として使用したい場合は、propsも`hoge`とします。
後述する`emit`の特別な記載方法によって、使用側が`v-model`ディレクティブを使用できるようになります。

#### setupについて
こちらは解説項目が多いため、ソースコードにコメントで記載しております。
```js
// setup関数の引数に props(表示制御に使用するため), context(emitで使用するため)を受け取る
setup(props, context) {
    // 該当要素がフォーカス状態であるかを保持するリアクティブな値を宣言
    const isFocus = ref(false);
    
    // isFocusに応じて表示する内容を決定する算出プロパティ
    const commaSeparetedValue = computed(() => {
      // null時には空白を表示させる
      // これがないと、入力値がnullの場合に0が表示される
      if (props.modelValue === null) {
        return "";
      }

      // カンマ区切りには標準のtoLocaleString()メソッドを使用
      return isFocus.value
        ? props.modelValue
        : Number(props.modelValue).toLocaleString();
    });

    // focusイベントで使用
    const onFocus = () => {
      isFocus.value = true;
    };

    // blurイベントで使用
    const onBlur = () => {
      isFocus.value = false;
    };

    //　inputイベントで使用
    const onInput = (event) => {
      const value = event.target.value;
      // 数値以外の入力を許容しない
      if (isNaN(value)) {
        return;
      }

      // emitの予約語である "update:modelValue(引数名)"を使用 これにより使用側がv-modelディレクティブを使える
      // 例: propsのhogeに対して使用側がv-modelを使用する場合、emitは以下のように記載
      // context.emit("update:hoge", value);
      // 上記の場合、使用側は v-model:hoge="hoge" として使用可能
      context.emit("update:modelValue", value);
    };

    return {
      commaSeparetedValue,
      onFocus,
      onBlur,
      onInput,
    };
  },
```

## まとめ
今回はカンマ区切りのinputコンポーネントの作成について解説しました。
上記のようなコンポーネントは、よくある要望だと思います。
しかし、Vue3系での情報などが少なかったため、記事にしてみました。

また、記載したコードのように`v-model`ディレクティブなどを自前で実装できるようになると、プロジェクトを通して共通的に使用するコンポーネントを作成する際のクオリティが上がるかと思います。
ぜひ使ってみてください!!