【React×TS】快適な送信フォームを作る

こんにちは、[Yui](https://twitter.com/yui_active)です。

React×TSで[react-hook-form](https://react-hook-form.com/)で快適な送信フォームを作ったので紹介します。
今回入れた機能は以下。

- 送信したら入力欄はブランクに戻す
- cmd+Enter(windowsだったらctrl+Enter)で送信できるようにする
- ただし入力が途中の場合は↑で送信できないようにする
- スペースのみでは送信できないようにする
- 送信できる場合のみ送信ボタンはactiveにする


## まずはreact-hook-formで基本のフォームを作成


```react

import { useForm } from 'react-hook-form'

type FormData = Readonly<{
  message: string
}>

const Form = () => {
  const { handleSubmit, register, reset } = useForm<FormData>()
  const submit = (data: FormData) => {
    // 送信ボタンを押したら入力欄をブランク状態に戻す
    reset({ message: '' })
  }
  return (
    <div>
      <form onSubmit={handleSubmit(submit)}>
        <textarea
          placeholder="メッセージを入力"
          name="message"
          ref={register({ required: true })}
        />
        <button>送信</button>
      </form>
    </div>
  )
}

export default Form

```

これで文字の入力と送信、そして送信したら入力欄はブランクに戻すという動きができました。

## cmd+Enter(windowsの場合ctrl+Enter)で送信できるようにする

次に、ショートカットキーで送信できるようにします。
毎回送信ボタンを押すのも面倒なので、ショートカットキーで送信できるようにしたいですね。
実装は単純で、keydownアクションを追加してショートカットキーが押されたかどうかを探知しているだけです。

``textarea``にonKeyDownでキーが押されたときのアクションを追加します。

ちなみにsubmitで送信する値に関しては、react-hook-formのwatchを使うと簡単に入力値をゲットすることができるのでそれを使っています。

```react
const Form = () => {
  const { handleSubmit, watch, register, reset } = useForm<FormData>()
  const watchMessage = watch('message')
  const submit = (data: FormData) => {
    // 送信ボタンを押したら入力欄をブランク状態に戻す
    reset({ message: '' })
  }
  const enterPost = (keyEvent) => {
    if (keyEvent.key === 'Enter' && (keyEvent.ctrlKey || keyEvent.metaKey)) {
      submit({ message: watchMessage })
    }
  }
  return (
    <div>
      <form onSubmit={handleSubmit(submit)}>
        <textarea
          placeholder="メッセージ..."
          name="message"
          ref={register({ required: true })}
          onKeyDown={enterPost}
        />
        <button>送信</button>
      </form>
    </div>
  )
}
```

これでcmd+Enter(windowsの場合ctrl+Enter)で送信できるようになりました。
ただ、このままだと**入力途中でも送信できてしまう**ので修正する必要があります。

## 入力が途中の場合はショートカットキーで送信できないようにする

日本語入力の場合は、入力してエンターキーを押すまでは入力値は確定しません。
予測変換を確認してる際などに、誤ってってショートカットキーを押した場合反応してしまうと使い勝手が悪いです。
そこで、入力途中の場合はショートカットキーは使えないようにする必要があります。

入力が完了したかどうかを確認するのに使えるのが、[CompositionEvent](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent)です。

日本語入力の場合、入力後変換が終わってエンターキーを押したら、compositionendがtrueになります。
それを利用します。

JSX内では入力が確定したらonCompositionEndイベントが走るのでそれとuseStateをあわせて以下のように書くことができます。

```react
import { useState } from 'react'

const Form = () => {
  const { handleSubmit, watch, register, reset } = useForm<FormData>()
  const [isCompositionend, setIsCompositionend] = useState<boolean>(false)
  const watchMessage = watch('message')
  const submit = (data: FormData) => {
    // 送信ボタンを押したら入力欄をブランク状態に戻す
    reset({ message: '' })
  }
  const enterPost = (keyEvent) => {
    // isCompositionendがfalseの場合は入力途中なので何もしない
    if (!isCompositionend) {
      return
    }
    if (keyEvent.key === 'Enter' && (keyEvent.ctrlKey || keyEvent.metaKey)) {
      submit({ message: watchMessage })
      // 送信したらisCompositionendは再度falseに戻しておく
      setIsCompositionend(false)
    }
  }
  return (
    <div>
      <form onSubmit={handleSubmit(submit)}>
        <textarea
          placeholder="メッセージ..."
          name="message"
          ref={register({ required: true })}
          onKeyDown={enterPost}
          onCompositionEnd={() => {
            setIsCompositionend(true)
          }}
        />
        <button>送信</button>
      </form>
    </div>
  )
}
```

これでだいたいできました。

が、このままではまだ問題があります。
それは、アルファベット入力の場合（というより変換が必要ない言語の場合）、onCompositionEndイベントが走らないということです。
ナンテコッタイ...。

このままではアルファベット入力した場合にonCompositionEndイベントが走らず、常にisCompositionendがfalseになってしまい送信ができません。
そこで、**日本語入力の場合のみisCompositionendを参照してそれ以外はisCompositionendの値に関わらずショートカットキーで送信する**ようにしたいと思います。

## 入力された文字が日本語かどうかを確認する

日本語かどうかの確認に以下の正規表現を使います。

```react
const jaRegexp = /^[\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf]+$/
{確認したい文字列}.match(jaRegexp)
```
enterPostの関数の中身を以下に書き換えます。

```react
  const enterPost = (keyEvent) => {
    // 入力値が日本語の場合でisCompositionendがfalseの場合は入力途中なので何もしない
    if (watchMessage.match(jaRegexp) && !isCompositionend) {
      return
    }
    if (keyEvent.key === 'Enter' && (keyEvent.ctrlKey || keyEvent.metaKey)) {
      submit({ message: watchMessage })
      setIsCompositionend(false)
    }
  }
```

これでやっと完成！と言いたいところですが、現在は空欄（スペース）のみの入力でも送信できてしまいます...。
これだと少し都合が悪いので、スペースのみの入力は送信できないように変更します。

## スペースのみでは送信できないようにする

スペースのみかどうかを判断するために、私は``trim()``を使いました。
今回だと、``watchMessage.trim()``がnullの場合はスペースのみと判定します。


```react
  const enterPost = (keyEvent) => {
    // 入力値がスペースのみ、又は入力値が日本語の場合でisCompositionendがfalseの場合は入力途中なので何もしない
    if (
      !watchMessage.trim() ||
      (watchMessage.match(jaRegexp) && !isCompositionend)
    ) {
      return
    }
    if (keyEvent.key === 'Enter' && (keyEvent.ctrlKey || keyEvent.metaKey)) {
      submit({ message: watchMessage })
      setIsCompositionend(false)
    }
  }
```

これで、ショートカットキーではスペースのみの場合は送信できないようになりました。
ただ、送信ボタンを押されるとまだスペースのみで送信できてしまうので、送信ボタンでもスペースのみで送信できないようにします。
また、ついでに送信できるかできないかが視覚的にわかりやすいほうが良いので、送信できる場合のみボタンをactiveモードにします。

## 送信できる場合のみ送信ボタンはactiveにする

cssを利用して、``watchMessage.trim()``がtrue（何かしらのスペースではない入力がある）場合のみ色を変えて送信できることをわかりやすくします。
また、初期状態では``pointer-events: none;``をつけてあげて入力が反応しないようにします。

※devツールで書き換えることができるので、確実にスペースの入力を許容しない場合はsubmit部分でもスペースのみの場合は何もしない処理に加えて、API側でもスペースのみを弾くなりしたほうが良いです。
今回はそこまで厳密にしなくていいのでざっくりcssだけでやります。

clsxを利用してwatchMessage.trim()がtrueの場合のみvalidというバリアントクラスを当てます。

```react

<button className={clsx("sendIcon", watchMessage?.trim() && "valid")}>送信</button>
```

```scss

  .sendIcon {
    color: gray;
    pointer-events: none;
    &.valid {
      color: blue;
      cursor: pointer;
      pointer-events: visible;
    }
  }
```

watchMessageのあとに?をつけているのはwatchMessageにそもそも何も入力されていない場合はtrim()できないので、そこでエラーが出るのを防ぐためです。

これで完成です。

ざっくりと書いたコードが以下です。
送信したことがわかりやすいように1秒間だけ送信しましたとメッセージが出るようになっていますが、どこにも送信されてないので安心して動作確認してみてください。

https://codesandbox.io/s/pedantic-platform-yn3f0?file=/src/App.tsx


もっとこうしたほうが良い！みたいな書き方などあれば教えて下さい〜。