【Vue.js】CompositionAPIでフォームを実装


はじめに

最近趣味で、Vue.js、CompositionAPIを試しています。

その一環として、フォームをできる限りライブラリを使わずに実装してみたので、記事に残したいと思います。

更新履歴

  • 2021/06/29: event.targetの参照方法を修正

フォームの概要

今回作成したフォームは、簡単な「ユーザー作成フォーム」です。

入力フィールドは「ユーザー名」と「メールアドレス」の2つです。

また、入力値に問題があれば、以下のようにエラーメッセージを表示します。

作成したソースコード

今回作成したソースコードは、以下のGithubリポジトリで公開しています。

Ushinji/vue-form-sample

入力フィールド / useField

動作

ユーザー作成フォームコンポーネントの実装の中で、まず入力フィールドから説明します。

入力フィールドは、input属性とエラーメッセージの2つからなります。

エラーメッセージは入力バリデーションがエラーの場合に表示されます。今回のバリデーションエラーの判定条件は「ユーザー名が空欄ではない」ことです。

しかし、入力フィールドの初期状態は空欄であるためエラー判定となります。ユーザーからすると、何も操作していないにも関わらずいきなりエラーメッセージを見せられるのは心象が良いとはいえません。

そのため、このフォームでは初期状態はバリデーションエラーとはせず、ユーザーが入力フィールドに操作が行われた後にエラー判定を始めるようにしました。具体的なエラー判定開始タイミングは、入力フィールドに対するfocusが外れたとき(Blur)です。

実装

以上を踏まえて、ここから実装について説明します。

まず、入力フィールドに関するロジックを定義したuseFieldです。

// useField.ts
import { ref, computed } from 'vue';

const useField = (
  initialValue: string,
  validate: (value: string) => boolean = () => false
) => {

  const value = ref(initialValue);
  const isTouched = ref(false);

  const error = computed(() => {
    return !validate(value.value);
  });

  const onInput = (event: Event) => {
    const { target } = event;
    if (target instanceof HTMLInputElement) {
      value.value = target.value;
    }
  };

  const onBlur = () => {
    isTouched.value = true;
  };

  return {
    props: { value, onInput, onBlur },
    meta: {
      isTouched,
      error,
    },
  };
};

export default useField;

useFiledは「入力フィールドの初期値」と「入力バリデーション判定式」の2つを渡すことで入力フィールドのロジックを返します。

useFieldが管理する状態は入力値(value)、ユーザーの入力フィールドの操作判定(isTouched)、バリデーションエラー判定(error)の3つです。これらはrefとcomputedを利用し、リアクティブな値として定義しています。

入力値のvalueはonInput関数によって値が更新されます。onInput関数は該当の入力フィールドの@inputに指定することで、入力値の変更をトリガーにonIput関数が発火しvalueが更新されます。

またisTouchedはonBlur関数によって値が更新されます。onBlurは該当の入力フィールドの@blurに指定し、入力フィールドに対するfocusが外れたタイミングでisTouchedを更新します。

<!-- useFieldのロジック指定方法 -->
<input :value="value" @input="onInput" @blur="onBlur" />

また、useFieldの戻り値はpropsmetaという名前で構造化しました。propsについては、該当の入力フィールドにinputに指定する属性を集約しています。また、metaはエラーといったいわゆるmeta情報を集めています。

useFieldを使ったForm実装

先ほど説明したuseFieldを使ってFormの入力フィールドを実装すると、以下のようなコードになります。


<!-- UserCreateForm.vue -->
<template>
<div class="field">
  <label class="label" for="user-name">ユーザー名</label>
  <div class="control">
    <input
      id="user-name"
      name="user-name"
      type="text"
      :value="userNameField.props.value.value"
      @input="userNameField.props.onInput"
      @blur="userNameField.props.onBlur"
    />
    <p
      class="help is-danger"
      v-show="
        userNameField.meta.isTouched.value && userNameField.meta.error.value
      "
    >
     ユーザー名を入力してください。
    </p>
  </div>
</div>
</template>

<script lang="ts">
import { createComponent } from '@vue/composition-api';
import useField from './useField';

export default createComponent({
  setup() {
    // ユーザー名フィールドの定義
    const userNameField = useField('', (value: string) => {
      return value.length > 0;
    });
    return { userNameField };
  },
});
</script>

<script>内でユーザー名のフィールド情報(userNameField)を初期化します。そして目的のinput属性にフィールド情報を設定します。

また、エラーメッセージについてはInputにフォーカスが離れた後、バリデーションエラーの場合に表示されるように定義しました。

Submit処理の実装

まず、Formの<script>部分のコードです。フォームで利用するSubmitメソッド(onSubmit)とフォームエラー判定を定義します。


<script lang="ts">
import { defineComponent } from 'vue';
import useUserCreateForm from './composition';

export default defineComponent({
  setup() {
    // 各フィールドの定義(バリデーションメソッドの詳細は後述する)
    const userNameField = useField('', presenceValidator);
    const emailField = useField('', emailValidator);

    // フォームのエラー判定。各フィールドにエラー情報を元に判定する。
    const error = computed(() => {
      return userNameField.meta.error.value || emailField.meta.error.value;
    });

    // submitメソッド。各フィールドの値を使い、サーバーにPOSTリクエストを送信する。
    const onSubmit = async () => {
      if (error.value) {
        return;
      }
      // 今回はサーバーリクエストは行っていない
      console.log(userNameField.props.value.value, emailField.props.value.value);
    };

    // 各フィールド情報とフォーム情報をtemplate層に渡す
    return {
      userNameField,
      emailField,
      onSubmit,
      meta: {
        error,
      },
    };
  },
});
</script>

上述で利用した各フィールドのバリデーションメソッドは以下の通りです。

export const presenceValidator = (value: string) => {
  return value.length > 0;
};

export const emailValidator = (value: string) => {
  const re = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
  return re.test(value);
};

次にフォームの<template>層の実装は以下の通りです。<script>層で定義したonSubmitを送信ボタンクリック時に実行されるように定義します。

また、フォーム全体のエラー判定を送信ボタンの:disabledに指定することで、エラー時にボタンクリックができないようにします(disalbed状態)。


<template>
  <div class="container">
    <form @submit.prevent>

      <!-- 各フィールドは省略 -->

      <div class="field">
        <p class="control">
          <button
            class="button is-success"
            type="submit"
            @click="onSubmit"
            :disabled="meta.error.value"
          >
            作成する
          </button>
        </p>
      </div>
    </form>
  </div>
</template>

終わりに

今回はVue.jsのComposition APIを使ったフォーム実装について説明しました。

今回の実装は、以下の2つのライブラリを参考にして実装しました。

自分が普段はReact & hooksでフロントエンドコードを書いているので、Reactライクなコーディングになっている気がします。

何はともあれ、読んだ方に少しでも参考になれば幸いです。