Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: adding custom errors docs #1215

Merged
merged 8 commits into from
Mar 4, 2025

Conversation

enyelsequeira
Copy link
Contributor

Hello @crutchcorn here are the docs for custom errors

for these docs i've used my example I created yesterday here, let me know if you need me to change something

import { useState } from "react";
import { useForm } from "@tanstack/react-form";

function App() {
  const [submittedData, setSubmittedData] = useState(null);

  const form = useForm({
    defaultValues: {
      username: "",
      age: 0,
      email: "",
      password: "",
    },
    validators: {
      // Example 1: String error values
      onChange: ({ value }) => {
        return {
          fields: {
            username:
              value.username && value.username.length < 3
                ? "Username must be at least 3 characters"
                : undefined,
            email:
              value.email && !value.email.includes("@")
                ? "Email must contain @"
                : undefined,
          },
        };
      },

      // Example 2: Non-string error values
      onBlur: ({ value }) => {
        return {
          fields: {
            // Number error value
            age:
              value.age < 18
                ? 18 - value.age // Numeric value representing years below 18
                : undefined,

            // Object error value with additional context
            email:
              value.email && !value.email.endsWith(".com")
                ? {
                    message: "Only .com emails are allowed",
                    severity: "warning",
                    code: 1001,
                  }
                : undefined,
          },
        };
      },

      // Example 3: Mixed error value types
      onSubmit: ({ value }) => {
        return {
          fields: {
            // Array of errors
            username:
              !value.username || value.username.length < 5
                ? [
                    "Username is required",
                    "Username must be at least 5 characters",
                  ]
                : undefined,

            // Boolean error value
            age:
              value.age <= 0
                ? true // Just a flag indicating an error
                : undefined,
          },
        };
      },
    },
    onSubmit: async ({ value }) => {
      setSubmittedData(value);
    },
  });

  console.log({
    formErrors: form,
  });

  return (
    <div className="container">
      <h2>TanStack Form - String & Non-String Error Values</h2>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
      >
        {/* Username field */}
        <div className="field">
          <label>Username:</label>
          <form.Field
            name="username"
            children={(field) => (
              <div>
                <input
                  value={field.state.value}
                  onChange={(e) => field.handleChange(e.target.value)}
                  onBlur={field.handleBlur}
                />

                {/* Display errors based on type */}
                {field.state.meta.errors.map((error, i) => {
                  if (typeof error === "string") {
                    return (
                      <div key={i} className="error string-error">
                        {error}
                      </div>
                    );
                  } else if (Array.isArray(error)) {
                    return (
                      <ul key={i} className="error array-error">
                        {error.map((err, j) => (
                          <li key={j}>{err}</li>
                        ))}
                      </ul>
                    );
                  }
                  return null;
                })}
              </div>
            )}
          />
        </div>

        {/* Age field */}
        <div className="field">
          <label>Age:</label>
          <form.Field
            name="age"
            children={(field) => (
              <div>
                <input
                  type="number"
                  value={field.state.value || ""}
                  onChange={(e) => {
                    const value =
                      e.target.value === "" ? 0 : Number(e.target.value);
                    field.handleChange(value);
                  }}
                  onBlur={field.handleBlur}
                />

                {/* Display errors based on type */}
                {field.state.meta.errors.map((error, i) => {
                  if (typeof error === "number") {
                    return (
                      <div key={i} className="error number-error">
                        You need {error} more years to be eligible
                      </div>
                    );
                  } else if (error === true) {
                    return (
                      <div key={i} className="error bool-error">
                        Age must be greater than zero
                      </div>
                    );
                  }
                  return null;
                })}
              </div>
            )}
          />
        </div>

        {/* Email field with disableErrorFlat */}
        <div className="field">
          <label>Email:</label>
          <form.Field
            name="email"
            disableErrorFlat
            children={(field) => {
              console.log({ EMAIL: field.state.meta });
              return (
                <div>
                  <input
                    type="email"
                    value={field.state.value}
                    onChange={(e) => field.handleChange(e.target.value)}
                    onBlur={field.handleBlur}
                  />
                  {field.state.meta.errorMap.onBlur?.message ? (
                    <p>DEMO</p>
                  ) : null}

                  {/* String error from onChange */}
                  {typeof field.state.meta.errorMap.onChange === "string" && (
                    <div className="error string-error">
                      <strong>onChange:</strong>{" "}
                      {field.state.meta.errorMap.onChange}
                    </div>
                  )}

                  {/* Object error from onBlur */}
                  {field.state.meta.errorMap.onBlur &&
                    typeof field.state.meta.errorMap.onBlur === "object" &&
                    !Array.isArray(field.state.meta.errorMap.onBlur) && (
                      <div className="error object-error">
                        <strong>onBlur:</strong>{" "}
                        {field.state.meta.errorMap.onBlur.message}
                        <small>
                          {" "}
                          (Code: {field.state.meta.errorMap.onBlur.code},
                          Severity: {field.state.meta.errorMap.onBlur.severity})
                        </small>
                      </div>
                    )}
                </div>
              );
            }}
          />
        </div>

        <form.Field
          name="password"
          validators={{
            onChange: ({ value }) => {
              const errors = [];
              if (value.length < 8) errors.push("Password too short");
              if (!/[A-Z]/.test(value)) errors.push("Missing uppercase letter");
              if (!/[0-9]/.test(value)) errors.push("Missing number");

              return errors.length ? errors : undefined;
            },
          }}
          children={(field) => {
            console.log({ field: field.state.meta.errors });
            return (
              <div>
                <input
                  type="password"
                  value={field.state.value}
                  onChange={(e) => field.handleChange(e.target.value)}
                  onBlur={field.handleBlur}
                />
                {field.state.meta.errorMap.onChange ? (
                  <p>{field.state.meta.errorMap.onChange.map((e) => e)}</p>
                ) : null}
                {Array.isArray(field.state.meta.errors) && (
                  <ul className="error-list">
                    {field.state.meta.errors.map((err, i) => (
                      <li key={i}>{err}</li>
                    ))}
                  </ul>
                )}
              </div>
            );
          }}
        />

        <button type="submit">Submit Form</button>
      </form>

      {/* Show form-level errors */}
      <form.Subscribe
        selector={(state) => state.errors}
        children={(errors) =>
          errors.length > 0 ? (
            <div className="form-errors">
              <h3>Form Errors:</h3>
              <pre>{JSON.stringify(errors, null, 2)}</pre>
            </div>
          ) : null
        }
      />

      {/* Submitted data display */}
      {submittedData && (
        <div className="result">
          <h3>Submitted Data:</h3>
          <pre>{JSON.stringify(submittedData, null, 2)}</pre>
        </div>
      )}

      <div className="explanation">
        <h3>Key Features Demonstrated:</h3>
        <ol>
          <li>
            <strong>String error values</strong> - Traditional text-based error
            messages
          </li>
          <li>
            <strong>Non-string error values</strong> - Numbers, booleans,
            objects, and arrays as error values
          </li>
          <li>
            <strong>disableErrorFlat prop</strong> - Access errors by validation
            source using errorMap
          </li>
          <li>
            <strong>Type-safe errors</strong> - The component correctly handles
            different error types
          </li>
        </ol>
      </div>
    </div>
  );
}

export default App;

@enyelsequeira enyelsequeira mentioned this pull request Mar 4, 2025
Copy link
Member

@Balastrong Balastrong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I really like the examples as they perfectly showcase why it's so cool to have such flexibility, well done!

I added a few comments to improve this even more, let me know what you think :)

@enyelsequeira
Copy link
Contributor Author

Hey @Balastrong great, thank you for the feedback. I have updated the parts you have mentioned. Let me know if more changes are required

@enyelsequeira
Copy link
Contributor Author

Hey @Balastrong it has been updated, thank you 😊

Copy link
Member

@Balastrong Balastrong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for creating this new page with a lot of useful info and examples :)

@Balastrong Balastrong merged commit 2f0a23f into TanStack:main Mar 4, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants