Sreekar Mouli
Sreekar Mouli

Reputation: 1432

antd - Form input dynamic validation

In my project, I am using antd Form in the login screen. If the user hasn't provided email or if the email is invalid, I am handling that using the rules attribute that antd's Form.Item supports.

Here is what I wanted to achieve:

I also wanted to handle another error which is 'No account associated with email'. After the user click on the login button, there is a back-end API call. When the backend returns this error in the response, then UI should show this error message under the email input.

To achieve this, I am storing this error message in a state variable 'emailError' and I'm using this in the 'rules' attribute of the Form.Item as follows:

function Login() {
  const [loginForm] = Form.useForm();
  const [emailError, setEmailError] = useState({ enable: false, helpText: "" });
  const emailErrorRef = useRef(emailError);

  const onLoginFormSubmit = async (values) => {
    try {
      let response = await loginRequest(values);
      console.log("Login -> onLoginFormSubmit -> response", response);
      // ...some logic
    } catch (err) {
      /*
        err.response.data = {
          code: 'ERR_LOGIN_NO_ACCOUNT_WITH_EMAIL',
          detail: 'No account associated with this email'
        }
      */
      let errCode = err.response.data.code;
      let errMessage = err.response.data.detail;
      // If the API fails with the below error, then update the state variable.
      if (errCode === "ERR_LOGIN_NO_ACCOUNT_WITH_EMAIL") {
        console.log(
          "Login -> update emailError -> ERR_LOGIN_NO_ACCOUNT_WITH_EMAIL"
        );
        setEmailError({
          enable: true,
          helpText: errMessage
        });
      }
    }
  };

  useEffect(() => {
    console.log("Login -> updated emailError", emailError);
    emailErrorRef.current = emailError;
  }, [emailError]);

  return (
    <div
      style={{
        position: "fixed",
        width: 400,
        height: 200,
        top: "calc(50% - 100px)",
        left: "calc(50% - 200px)"
      }}
    >
      <Form form={loginForm} onFinish={onLoginFormSubmit}>
        <Form.Item
          name="email"
          rules={[
            {
              type: "email",
              message: "Please provide a valid email!"
            },
            { required: true, message: "Please provide your email!" },
            () => ({
              validator() {
                console.log(
                  "Inside email validator! -> emailError",
                  emailError
                );
                if (emailError.enable) {
                  return Promise.reject(emailError.helpText);
                }
                return Promise.resolve();
              }
            })
          ]}
        >
          <Input autoFocus placeholder="Email" prefix={<EmailIcon />} />
        </Form.Item>
        <Form.Item
          name="password"
          rules={[{ required: true, message: "Please provide your password!" }]}
        >
          <Input.Password placeholder="Password" prefix={<PasswordIcon />} />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">
            Login
          </Button>
        </Form.Item>
      </Form>
    </div>
  );
}

From what I can understand based on the logs, I think that the Form is not being re-rendered when I am updating the state variable 'emailError' after receiving the response from async API call.

UPDATE: I am also logging the state variable 'emailError' in the HTML. The change is state variable is reflected in the UI. So, I think that the re-rendering is working fine. But, during the re-render, the rules are not being validated..

I have tried to use 'emailErrorRef.current' instead of 'emailError' in the validator() but this didn't work.

What am I missing here? Please help fix this!

Here is a codesandbox which replicates this:

Edit unruffled-darkness-vjo00

Upvotes: 2

Views: 6724

Answers (2)

mandy
mandy

Reputation: 1

import "../css/login.css";
import { LockOutlined, UserOutlined } from "@ant-design/icons";
import { Button, Checkbox, Form, Input, Card, Space, Image } from "antd";
import logo from "../images/logo.png";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

function LoginSample() {
  const navigate = useNavigate();
  const [email, setEmail] = useState("");
  const [emailError, setEmailError] = useState("");
  const [password, setPassword] = useState("");
  const [passwordError, setPasswordError] = useState("");
  const [successMsg, setSuccessMsg] = useState("");

  const handleEmailChange = (e) => {
    setSuccessMsg("");
    setEmailError("");
    setEmail(e.target.value);
  };

  const handlePasswordChange = (e) => {
    setSuccessMsg("");
    setPasswordError("");
    setPassword(e.target.value);
  };

  const handleFormSubmit = (e) => {
    //empty email
    if (email !== "") {
      const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
      //type of email
      if (emailRegex.test(email)) {
        setEmailError("");
        //correct email or not
        if (email == "[email protected]") {
          setEmailError("");
          if (password == "admin") {
            navigate("/dashboard");
          } else {
            setPasswordError("password incorect");
          }
        } else {
          setEmailError("email incorrect");
        }
      } else {
        setEmailError("Invalid Email");
      }
    } else {
      setEmailError("Email Required");
    }
    if (password !== "") {
    } else {
      setPasswordError("Password Required");
    }
  };

  return (
    <Space
      direction="horizontal"
      style={{ width: "100%", justifyContent: "center", marginTop: "150px" }}
    >
      <Card
        style={{
          width: 350,
          margin: "20px",
          borderRadius: "20px",
          overflow: "hidden",
        }}
      >
        <Space
          direction="horizontal"
          style={{ width: "100%", justifyContent: "center" }}
        >
          <Image width={200} src={logo} preview={false} />
        </Space>
        <Form
          name="normal_login"
          className="login-form"
          initialValues={{
            remember: true,
          }}
        >
          <Form.Item>
            <Input
              type="email"
              onChange={handleEmailChange}
              value={email}
              prefix={<UserOutlined className="site-form-item-icon" />}
              placeholder="Email"
            />
            {emailError && <Space className="error-msg">{emailError}</Space>}
          </Form.Item>

          <Form.Item>
            <Input
              type="password"
              onChange={handlePasswordChange}
              value={password}
              prefix={<LockOutlined className="site-form-item-icon" />}
              placeholder="Password"
            />

            {passwordError && (
              <Space className="error-msg">{passwordError}</Space>
            )}
          </Form.Item>

          <Form.Item>
            <Form.Item name="remember" valuePropName="checked" noStyle>
              <Checkbox>Remember me</Checkbox>
            </Form.Item>

            <a className="login-form-forgot" href="">
              Forgot password
            </a>
          </Form.Item>

          <Form.Item>
            <Button
              type="primary"
              onClick={() => handleFormSubmit()}
              className="login-form-button"
            >
              Log in
            </Button>
            <Space
              direction="horizontal"
              style={{ width: "100%", justifyContent: "center" }}
            >
              OR
            </Space>
            <Space
              direction="horizontal"
              style={{ width: "100%", justifyContent: "center" }}
            >
              <a href="">Register for new account</a>
            </Space>
          </Form.Item>
        </Form>
      </Card>
    </Space>
  );
}

export default LoginSample;

Upvotes: 0

Saeed Shamloo
Saeed Shamloo

Reputation: 6554

Your validation calls when you apply a change on one of the fields and when you submit the form, so when you call your api and get the error, you'r validation won't call until apply a change on form or submit it again, a solution could be calling validation programatically by using validateFields on antd form. like this:

useEffect(() => {
  if(emailError.enable){
    loginForm.validateFields();
  }
}, [emailError]);
...
<Form form={loginForm} 
    onChange={()=> {
      if(emailError.enable)
        setEmailError({enable:false, helpText:''})
    }}
...></Form>

Here is the full example:

Edit unruffled-darkness-vjo00

Upvotes: 2

Related Questions