3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel Precognition】Laravel + React + Inertia環境でライブバリデーションをつくる

Posted at

はじめに

今回は、Laravel Precognitionを使用したライブバリデーションについてまとめました。

※おことわり※
基本的に学習内容のアウトプットです。
初学者であるため、間違い等あればご指摘いただけますと嬉しいです。

この記事の目的

以下内容のアウトプット

  • Laravel Precognitionを使用したバリデーションの実装

この記事の内容

  1. Precognitionについて
  2. 前提
  3. ざっくり
  4. ルートの作成
  5. バリデーションの作成
  6. コントローラの作成
  7. Precognitionライブラリをインストール
  8. ビューの作成
  9. 参考

1. Precognitionについて

One of the primary use cases of Precognition is the ability to provide "live" validation for your frontend JavaScript application without having to duplicate your application's backend validation rules.

バックエンドと同じバリデーションルールを、複製することなくフロントエンドに適用することができます。
リアルタイムで検証が行われ、ライブバリデーションを実現できます。

ライブバリデーションとは

こんな感じで、laravelのFormRequestで実装した検証内容をReactに適用できます。

Image from Gyazo

2. 前提

  • Inertia環境のLaravelプロジェクトが構築済み ※1
  • Material UIがインストール済み ※2

※1 構築がまだの方は以下から環境構築できます。

※2 以下コマンドを実行し、Material UIと関連するモジュールをインストールします。

npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

3. ざっくり

名前、題名、説明の3点が入力できるフォームを作成します。
フォームの検証にはライブバリデーションを適用し、どのように動くか検証します。

4. ルートの作成

以下2つのルートを作成します。

  • フォーム
  • フォームの送信先
web.php

// フォーム
Route::get('/form', [FormController::class, 'form'])->name('precognition.form');

// フォームの登録 
Route::post('/create', [FormController::class, 'create'])->middleware([HandlePrecognitiveRequests::class])->name('precognition.create');
                                                        // ^^^^^^^^^^^ Precognitionを有効にする

create()メソッドに対して、middlewareを指定してPrecognitionを有効にします。

5. バリデーションの作成

FormRequestを作成します。

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'userName' => ['required', 'max:10'],
            'title' => ['required', 'max:150'],
            'content' => ['required', 'max:1500'],
        ];
    }

    public function messages()
    {
        return [
            'userName.required' => '名前を入力してください。',
            'userName.max' => '名前は10文字以内で入力をしてください。',
            'title.required' => '題名を入力してください。',
            'title.max' => '題名は150文字以内で入力をしてください。',
            'content.required' => '説明を入力してください。',
            'content.max' => '説明は1500文字以内で入力をしてください。',
        ];
    }
}

キー名(userNameなど)はフロントのフィールドに合わせます。

6. コントローラの作成

コントローラを作成し、form()とcreate()メソッドを定義します。

php artisan make:controller FormController
FormController.php
class FormController extends Controller
{
  /**
   * フォームを表示
   * @return Response
   */
  public function form(): Response
  {
      return Inertia::render('Form/FormCreate');
  }

  /**
   * フォームの登録
   * @param PrecognitionFormRequest $request
   * @return RedirectResponse
   */
  public function create(PrecognitionFormRequest $request): RedirectResponse
  {
    // 登録処理を省略
    return to_route('hoge.huga');
  }

create()メソッドにPrecognitionFormRequestを指定します。

7. Precognitionライブラリをインストール

laravel-precognition-react-inertiaパッケージをインストールします。

npm install laravel-precognition-react-inertia

この後の実装で、検証機能が強化されたInertiaのフォームヘルパー useForm を使用します。

8. ビューの作成

Material UIを使用して、フォームを作成します。

FormCreate.tsx
import { ChangeEvent } from 'react';
import { useForm } from 'laravel-precognition-react-inertia'; // ※1
import {
    Box,
    Button,
    Card,
    CardContent,
    FormControl,
    Grid,
    TextField,
    Typography,
} from '@mui/material';
import InputError from '@/Components/InputError';

type FormCreateProps = {
    userName: string;
    title: string;
    content: string;
};

export const FormCreate = () => {
    // フォームの初期化 
    const form = useForm<FormCreateProps>('post', route('precognition.create'), { // ※1
        userName: '',
        title: '',
        content: '',
    });

    // 送信
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
        e.preventDefault();
        form.submit({
            preserveScroll: true,
            onSuccess: () => {
                form.reset();
            },
            onError: (errors: object) => {
                console.error(errors);
            },
        });
    };

    // 入力項目のonChangeイベント
    const handleChangeUserName = (e: ChangeEvent<HTMLInputElement>): void => {
        form.setData('userName', e.target.value);
        form.forgetError('userName');
    };
    const handleChangeTitle = (e: ChangeEvent<HTMLInputElement>): void => {
        form.setData('title', e.target.value);
        form.forgetError('title');
    };
    const handleChangeContent = (e: ChangeEvent<HTMLInputElement>): void => {
        form.setData('content', e.target.value);
        form.forgetError('content');
    };

    return (
        <Box component="form" onSubmit={handleSubmit}> // ※2
            <Card variant="outlined" sx={{ mt: 5 }}>
                <CardContent>
                    <Typography
                        variant="h6"
                        component="div"
                        sx={{ mb: 1, fontWeight: 'bold', textAlign: 'center' }}
                    >
                        フォーム
                    </Typography>
                    <Grid container sx={{ my: 2 }}>
                        <TextField
                            label={'名前'}
                            required={true}
                            value={form.data.userName}
                            onChange={handleChangeUserName} // ※3
                            onBlur={() => form.validate('userName')} // ※4
                            fullWidth
                            inputProps={{
                                style: {
                                    boxShadow: 'none',
                                },
                            }}
                        />
                        {form.invalid('userName') && (
                            <InputError message={form.errors.userName} className="mt-2" />
                        )} // ※5
                    </Grid>
                    <Grid container sx={{ my: 2 }}>
                        <TextField
                            label={'題名'}
                            required={true}
                            value={form.data.title}
                            onChange={handleChangeTitle}
                            onBlur={() => form.validate('title')}
                            fullWidth
                            inputProps={{
                                style: {
                                    boxShadow: 'none',
                                },
                            }}
                        />
                        {form.invalid('title') && (
                            <InputError message={form.errors.title} className="mt-2" />
                        )}
                    </Grid>
                    <Grid container sx={{ my: 2 }}>
                        <TextField
                            label={'説明'}
                            required={true}
                            value={form.data.content}
                            onChange={handleChangeContent}
                            onBlur={() => form.validate('content')}
                            fullWidth
                            inputProps={{
                                style: {
                                    boxShadow: 'none',
                                },
                            }}
                        />
                        {form.invalid('content') && (
                            <InputError message={form.errors.content} className="mt-2" />
                        )}
                    </Grid>
                    <Grid container>
                        <Button disabled={form.processing} type={'submit'} variant="contained"> // ※6
                            登録
                        </Button>
                    </Grid>
                </CardContent>
            </Card>
        </Box>
    );
};

※1 useForm

  • reactのuseFormではなく、laravel-precognition-react-inertiaの useForm を使用する
  • useFormを使用して フォームオブジェクト を作成する
  • 各フィールドに初期値を指定する

※2 onSubmit 

  • formタグでフォームを囲う
  • onSubmitでデフォルトのフォーム送信を防ぎ、 form.submit をトリガーする

※3 onChange

  • 入力値が変更されたとき、 form.setData で新しい値をセットする
  • form.forgetError でエラーを手動でクリアする

※4 onBlur 

  • 対応するフィールドのバリデーションをトリガーする

※5 エラーメッセージ

  • 検証エラーかどうか form.invalid で判定をして、検証エラーの場合、指定したフィールドのエラーメッセージを表示する

※6 多重クリック対策

  • form.processing でフォーム送信リクエストが処理中かどうか判定する
  • 送信中の場合、ボタンが非活性になり、クリックされるのを防ぐ

Image from Gyazo

フォームオブジェクト

  • form.reset:フォームをリセットする

  • form.processing:フォーム送信リクエストが処理中の場合、falseを返す

  • form.data:フォームの値を管理する

    console.log(form.data);
    

    image.png

  • form.validating:検証リクエストが進行中の場合、trueを返す

  • form.hasErrors:フォームにエラーがある場合、trueを返す

  • form.valid:検証を通過した場合、trueを返す

  • form.invalid:検証エラーの場合、trueを返す

  • form.errors:検証エラーの場合、エラーが格納される

    console.log(form.errors);
    

    image.png

完成したフォーム

image.png

バリデーションの挙動

Image from Gyazo

9. 参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?