React + MUI(Material-UI)を使って簡単なフォームを作っていたのですが、さあバリデーション(Validation)はどうしようかな?と思い、ネットを調べだしました。
そもそもReactを使いフォームを作る際にはReact Hook Formが定番ですが、MUIと組み合わせるにはControllerを使う必要があり、複雑度が上がるので(使わない方法もあるらしいです)今回はReact Hook Formの採用は見送りました。
さてさて、バリデーション(Validation)に調べると10 Best JavaScript Validation Libraries in 2022 Openbaseなどたくさんの記事があります、日本語でもReactで使えるバリデーションライブラリを紹介!などがあり、どうしようか迷っていました。
HTML5 Form Validation
私はPerlやJavaの時代から長らくWebアプリを作っているので「Validationはプログラムで行う事」という常識がありました。しかし、ネットを調べていてHTML5で追加されたクライアント側の検証(Client-side validation)を発見しました!😅
HTML5ではブラウザーにバリデーションの機能が組み込まれています。詳しくはクライアント側のフォームデータ検証(Client-side form validation)に書かれていますが、
- 必須項目の指定、inputタグ等のrequired属性
- 文字数や値の下限・上限の指定、inputタグ等のmin,maxlength属性など
- 正規表現での文字列パターンの指定、inputタグ等ののpattern属性
- エラーメッセージ
- エラーの表示(CSS)機能
- JavaScript用API、inputタグ等のvalidityプロパティなど
- JavaScriptを使った独自チェック、setCustomValidity()メソッド
などがサポートされています。簡単なJavaScriptを組み合わせるだけでバリデーションが実現できるのです。
MUIのHTML5 Form Validationサポート
MUIの<TextField>
等はinputProps属性で、required, min, patternなどのHTML5 Form Validation用属性を指定できます。また、カッコ良くエラーメッセージを表示するための
- errorプロパティー(bool)、エラーを示すハイライト表示設定
- helperText(node, string)、エラーメッセージの設定
があります。これを組み合わせると、HTML5 Form Validationが使えます。
サンプルコード
実際のコードはRedux Toolkitを使っていますが、ここでは下図のようなuseState
を使った説明用のコードを作りました。
- ① メールアドレスチェック用正規表現、クライアント側の検証から持ってきました
- ② inputタグのDOM属性をアクセスするための
useRef
- ③ 入力値を保持するstate
- ④ エラー有無を保持するstate
- ⑤ 全フォームの検証を行う関数
- ⑥ Email入力input(TextField)のrefが設定されているか
- ⑦ Email入力inputにHTML5バリデーション結果の取得
- ⑧ バリデーション結果(エラー状態)をstateへ設定
- ⑨ 全フォームが正しければvalidがtrueにする
- ⑩ パスワード入力input(TextField)のHTML5バリデーションのコード
- ⑪ パスワード確認入力input(TextField)のHTML5バリデーションのコード
- ⑫ パスワード入力、パスワード確認入力が等しくなければ、独自エラーsetCustomValidityを設定
- ⑬ Email等と同様のHTML5バリデーションのチェック
- ⑭ 全フォームが正しいかを戻す
import { Button, Container, TextField } from "@mui/material";
import React, { useRef, useState } from "react";
type OnChangeEvent = React.ChangeEvent<HTMLInputElement>;
const EmailVaildPattern = // ← ①
"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$";
const App = () => {
const emailRef = useRef<HTMLInputElement>(null); // ← ②
const passwordRef = useRef<HTMLInputElement>(null);
const confirmPasswordRef = useRef<HTMLInputElement>(null);
const [emailValue, setEmailValue] = useState(""); // ← ③
const [passwordValue, setPasswordValue] = useState("");
const [confirmPasswordValue, setConfirmPasswordValue] = useState("");
const [emailError, setEmailError] = useState(false); // ← ④
const [passwordError, setPasswordError] = useState(false);
const [confirmPasswordError, setConfirmPasswordError] = useState(false);
const formValidation = (): boolean => { // ← ⑤
let valid = true;
const e = emailRef?.current; // ← ⑥
if (e) {
const ok = e.validity.valid; // ← ⑦
setEmailError(!ok); // ← ⑧
valid &&= ok; // ← ⑨
}
const p = passwordRef?.current; // ← ⑩
if (p) {
const ok = p.validity.valid;
setPasswordError(!ok);
valid &&= ok;
}
const c = confirmPasswordRef?.current; // ← ⑪
if (c) {
if (confirmPasswordValue.length > 0 && // ← ⑫
passwordValue !== confirmPasswordValue) {
c.setCustomValidity("パスワードが一致しません");
} else {
c.setCustomValidity("");
}
const ok = c.validity.valid; // ← ⑬
setConfirmPasswordError(!ok);
valid &&= ok;
}
return valid; // ← ⑭
};
return (
<Container component="main" maxWidth="xs">
<TextField
margin="normal"
fullWidth
required
inputRef={emailRef} // ← ⑮
value={emailValue} // ← ⑯
error={emailError} // ← ⑰
helperText={emailError && emailRef?.current?.validationMessage} // ← ⑱
inputProps={ {required: true, pattern: EmailVaildPattern} } // ← ⑲
onChange={(e: OnChangeEvent) => setEmailValue(e.target.value)} // ← ⑳
label="Email"
/>
<TextField
margin="normal"
fullWidth
required
type="password"
inputRef={passwordRef}
value={passwordValue}
error={passwordError}
helperText={passwordError && passwordRef?.current?.validationMessage}
inputProps={ {required: true} }
onChange={(e: OnChangeEvent) => setPasswordValue(e.target.value)}
label="Password"
/>
<TextField
margin="normal"
fullWidth
required
type="password"
inputRef={confirmPasswordRef}
value={confirmPasswordValue}
error={confirmPasswordError}
helperText={confirmPasswordError &&
confirmPasswordRef?.current?.validationMessage}
inputProps={ {required: true} }
onChange={(e: OnChangeEvent) => setConfirmPasswordValue(e.target.value)}
label="Confirm password"
/>
<Button
variant="contained"
fullWidth
sx={ {mt: 3} }
onClick={() => { // ← ㉑
if (formValidation()) {
alert("OK!");
}
}}
>
Register
</Button>
</Container>
);
};
export default App;
- ⑮
TextField
の使うinputタグのrefを指定 - ⑯ 入力値の設定
- ⑰ エラー状態の設定、エラーなら入力が赤色になる
- ⑱ エラーメッセージの指定、inputタグからvalidationMessageを取得
- ⑲ HTML5バリデーションの指定、ここでは必須項目, Emailのパターン を指定
- ⑳ キー入力された文字をstateに設定
- ㉑ Registerボタンを押したさいにバリデーションが実行され、エラーがなければアラートのOKが表示されます
まとめ
多数の入力があり複雑なバリデーションを行う場合や、HTML5未対応のブラウザーに対応するには10 Best JavaScript Validation Libraries in 2022 Openbase や Reactで使えるバリデーションライブラリを紹介! にあるようなライブラリーを使う事を検討した方が良いかと思いますが、シンプルなバリデーションだけならHTML5 Form Validationで充分かなと思いました。