EY-Office ブログ

先週のサンプルコードにバグがあったので修正

先週書いたHTML5 Form Validationのことを思い出したのでMUIで使ってみたですが、小さな(?)バグがあったので修正してみました。

本当の事を書くと、先週ブログを書く前にこのバグはわかっていたのですが、この修正をしてしまうと HTML5 Form Validation(Client-side validation)の基本から少しずれるかなと考え、そのまま書きました。

HTML5 Form Validation

バグの再現

バグの再現画面をアニメーションGIFにしてました。

  1. フォームな何も入力せずREGISTERボタンを押すと、Email入力欄に「このフィールドを入力してください。」とエラーが表示される
  2. そこでEmail入力欄にxxと入力するとエラーメッセージが「指定された形式で入力してください。」にかわる
  3. さらにxx@zzと入力するとエラーメッセージが消える、ただし入力欄はエラー状態(赤字)のまま

このプログラムではバリデーションはREGISTERボタンを押した時にに行われますが、HTML5 Form Validationはリアルタイム(キー入力が行われた)にバリデーションが行われるのでこの現象が起きます。

下に先週のコードを置きました。

  • ① REGISTERボタンが押されformValidation関数が実行されるとエラーが検出され、ReactのemailErrorステートがtrueになります
  • ② emailErrorステートがtrueの時は、Email入力欄のHTML5 ValidationのエラーメッセージがMUIコンポーネント(React)に結び付けるているのでリアルタイムでのバリデーション結果が表示されてしまいます
  • また、HTML5 Form Validationが正常になってもReactのステータスが更新されないので、エラー状態(赤字)のままになってしまいます

先週のコード

  const formValidation = (): boolean => {

・・・

    const e = emailRef?.current;
    if (e) {
      const ok = e.validity.valid;
      setEmailError(!ok);          // ←   ①
      valid &&= ok;
    }

・・・

      <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"
      />

サンプルコード

さてバグの原因は上の②なので

  • ③ エラーメッセージをステート(state)に持ちます
  • ④ エラーが検出された時点でのバリデーション・エラーメッセージがステートの保存されます
  • ⑤ エラーの有無はエラーメッセージの有無をTextFieldコンポーネントに渡す
  • ⑥ エラーメッセージはそのままTextFieldコンポーネントに渡せます
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("");                      // ← ③
  const [passwordError, setPasswordError] = useState("");
  const [confirmPasswordError, setConfirmPasswordError] = useState("");

  const formValidation = (): boolean => {
    let valid = true;

    const e = emailRef?.current;
    if (e) {
      const ok = e.validity.valid;
      setEmailError(ok ? "" : e.validationMessage);                      // ← ④
      valid &&= ok;
    }
    const p = passwordRef?.current;
    if (p) {
      const ok = p.validity.valid;
      setPasswordError(ok ? "" : p.validationMessage);
      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 ? "" : c.validationMessage);
      valid &&= ok;
    }

    return valid;
  };

  return (
    <Container component="main" maxWidth="xs">
      <TextField
        margin="normal"
        fullWidth
        required
        inputRef={emailRef}
        value={emailValue}
        error={emailError !== ""}                                     // ← ⑤
        helperText={emailError}                                       // ← ⑥
        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}
        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}
        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;

まとめ

HTML5 Form Validationはリアルタイムに行われるので、このコードのようにボタンを押したときだけ実行されるコードではなく、素直にリアルタイム・バリデーションにした方が良いのかも知れません。

しかし私は、リアルタイム・バリデーションはウザい感じがて好きになれないのです😅

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ