Laravel+Vue/Vuetifyで非同期バリデーション

前回作成したVue/Vuetifyのフォームにおける登録処理時のLaravel側での非同期バリデーションの実装です。具体的にはAxiosで非同期でフォーム値を送信し、LaravelのRequestでバリデーションを行いエラーがあればAxiosのレスポンスに返してコンポーネントにエラー表示させます。

開発環境

Laradock v10
Laravel 7
Vue 2.6

Laravel:モデル作成

登録もできるようにテーブルとモデルも作っておきましょう。

# php artisan make:model Models/Item --migration
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->id();
            $table->string('textbox', 255);
            $table->text('textarea');
            $table->tinyInteger('radiobtn');
            $table->tinyInteger('select');
            $table->string('checkbox', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}

※何か違うと思ったら、Laravel7では $table->id() は $table->bigIncrements(‘id’) のエイリアスらしい。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    protected $fillable = [
        'textbox', 'textarea', 'radiobtn', 'select', 'checkbox',
    ];
}

Laravel:フォームリクエスト作成

# php artisan make:request Ajax/User/ItemRequest
<?php

namespace App\Http\Requests\Ajax\User;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class ItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'textbox' => 'required|max:16',
            'textarea' => 'required|max:100',
            'radiobtn' => 'required',
            'select' => 'required',
            'checkbox' => 'required',
        ];
    }

    public function messages()
    {
        return [
            'textbox.required' => ':attributeは必須項目です。',
            'textbox.max' => ':attributeは16文字以内です。',
            'textarea.required' => ':attributeは必須項目です。',
            'textarea.max' => ':attributeは100文字以内です。',
            'radiobtn.required' => ':attributeは必須項目です。',
            'select.required' => ':attributeは必須項目です。',
            'checkbox.required' => ':attributeは1つ必ず選択してください。',
        ];
    }

    public function attributes()
    {
        return [
            'textbox' => 'テキストボックス',
            'textarea' => 'テキストエリア',
            'radiobtn' => 'ラジオボタン',
            'select' => 'セレクトボックス',
            'checkbox' => 'チェックボックス',
        ];
    }

    /**
     * [override] バリデーション失敗時ハンドリング
     *
     * @param Validator $validator
     * @throw HttpResponseException
     * @see FormRequest::failedValidation()
     */
    protected function failedValidation(Validator $validator) {
        $response['status']  = 400;
        $response['statusText'] = 'Failed validation.';
        $response['errors']  = $validator->errors();
        throw new HttpResponseException(
            response()->json( $response, 200 )
        );
    }
}

failedValidationメソッドをオーバーライドしてバリデーション失敗時のレスポンスを設定します。

Laravel:コントローラー作成

RESTfulなコントローラーを作成します。

# php artisan make:controller Ajax/User/ItemController --resource

登録処理(storeメソッド)を作成

<?php

namespace App\Http\Controllers\Ajax\User;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Http\Requests\Ajax\User\ItemRequest;
use App\Models\Item;

class ItemController extends Controller
{
    public function store(ItemRequest $request)
    {
        $status = 200;
        $message = null;

        $data = $request->all();
        $data['checkbox'] = implode(',', $data['checkbox']);	// 配列をカンマ区切りのテキストに変換
        $item = new Item();
        $result = $item->fill($data)->save();

        return response()->json(['post' => $data, 'status' => $status, 'message' => $message]);
    }
}

Laravel:ルーティング

Route::middleware('verified')->group(function() {
    Route::get('/user/{any?}', function() {
        return view('user');
    })->where('any', '.*');

    Route::resource('/ajax/user/item', 'Ajax\User\ItemController');	// 追加
});

Vue:フォームデータ送信/レスポンス処理

・各フォーム部品のエラー初期化
・フォーム送信機能(submit)を実装し、バリデーションエラーが帰ってきたらエラー情報にセット。
・次項のエラー表示で使用するエラーのリセット機能(clearError)の実装

<script>
  export default {
    data() {
      return {

・・・
        // エラー情報初期化
        errors: {
          textbox: false,	// 追加
          textarea: false,	// 追加
          radiobtn: false,	// 追加
          select: false,	// 追加
          checkbox: false,
        },
        messages: {
          textbox: null,	// 追加
          textarea: null,	// 追加
          radiobtn: null,	// 追加
          select: null,		// 追加
          checkbox: null,
        }
      }
    },

・・・

    methods: {
      // 送信ボタンを押したとき
      submit() {
        // 全てのエラーをリセット
        Object.keys(this.errors).forEach((key) => {
          this.errors[key] = false;
          this.messages[key] = null;
        })
        // 送信処理
        axios.post('/ajax/user/item', this.forms)
        .then((res) => {
          let response = res.data;
          if (response.status == 400) {
            // バリデーションエラー
            Object.keys(response.errors).forEach((key) => {
              this.errors[key] = true;
              this.messages[key] = response.errors[key];
            })
          } else {
            // 成功したらUserItemコンポーネントを表示
            this.$router.push('/user/item');
          }
        })
        .catch((error) => {
          console.log(error.response)
        })
      },
      // 各エラーのリセット
      clearError(item) {
        this.errors[item] = false;
        this.messages[item] = null;
      },

・・・

Vuetify:エラー表示

各フォーム部品に :error と :error-messages を追加します。

また、一度エラーを表示させると表示されたままになります。
エラーをリセットしないと送信ボタンが有効にならないので
テキストボックスとテキストエリアには @keydown
ラジオボタンとセレクトボックスには @change
で入力値の変更を監視してエラーをリセットします。

なお、チェックボックスについて変更の監視は 前回実装済です。

    <v-text-field
      v-model="forms.textbox"
      label="テキストボックス"
      :rules="[rules.required, rules.max_16]"
      :error="errors.textbox"					// 追加
      :error-messages="messages.textbox"		// 追加
      @keydown="clearError('textbox')"			// 追加
    ></v-text-field>

    <v-textarea
      v-model="forms.textarea"
      label="テキストエリア"
      auto-grow
      :rules="[rules.required, rules.max_100]"
      :error="errors.textarea"					// 追加
      :error-messages="messages.textarea"		// 追加
      @keydown="clearError('textarea')"			// 追加
    ></v-textarea>

    <v-radio-group
      v-model="forms.radiobtn"
      label="ラジオボタン"
      row
      :rules="[rules.required]"
      :error="errors.radiobtn"					// 追加
      :error-messages="messages.radiobtn"		// 追加
      @change="clearError('radiobtn')"			// 追加
    >
      <v-radio
        v-for="(name, index) in constant.RADIOS"
        :key=index
        :label=name
        :value=index
      ></v-radio>
    </v-radio-group>

    <v-select
      v-model="forms.select"
      label="セレクトボックス"
      :items="constant.SELECTS"
      item-text=name
      item-value=id
      :rules="[rules.required]"
      :error="errors.select"					// 追加
      :error-messages="messages.select"			// 追加
      @change="clearError('select')"			// 追加
    ></v-select>

    <v-container>
      <v-row>
        <div
          :class="errors.checkbox ? `theme--light v-label error--text` : `theme--light v-label`"
          style="margin-bottom: 0.5rem;"
        >チェックボックス</div>
      </v-row>
      <v-row>
        <v-checkbox
          v-model="forms.checkbox"
          v-for="(name, index) in constant.CHECKS"
          :key=index
          :label=name
          :value=index
          style="margin: 0 16px 0 0;"
          :rules="[rules.check_least_1]"
          :error="errors.checkbox"
          hide-details
          @change="changeCheckbox"
        ></v-checkbox>
      </v-row>
      <div
        v-for="(message, index) in messages.checkbox"
        :key=index
        class="v-messages error--text row"
      >{{ message }}</div>
    </v-container>

バリデーションテスト

Vue/Vuetify側のバリデーションを無効化してテストします。

        rules: {
/*
          required: value => !!value || '必須です。',
          max_16: value => value.length <= 16 || '16文字以内です。',
          max_100: value => value.length <= 100 || '100文字以内です。',
          check_least_1: value => {
            return value.length > 0 || '1つは必須選択です。'
          },
*/
required: true,
max_16: true,
max_100: true,
check_least_1: true,

ラジオボタンをクリアして送信ボタンを押したら、Laravelのバリデーションで設定したメッセージが出たらOKです。

laravel-vue-vuetify-validation