Coder Social home page Coder Social logo

enthusiastic-js / form-observer Goto Github PK

View Code? Open in Web Editor NEW
7.0 1.0 0.0 1.26 MB

A simple utility for reacting to events from a form's fields

License: MIT License

TypeScript 81.73% JavaScript 18.07% Svelte 0.19%
form html js observer progressive-enhancement react solid svelte validation vue web lit preact

form-observer's Introduction

Form Observer

A simple utility for reacting to events from a form's fields.

Features and Benefits

  • Performant: The Form Observer leverages event delegation to minimize memory usage. Moreover, it easily integrates into any JS framework without requiring state -- giving your app a significant boost in speed.
  • No Dependencies: The Form Observer packs a lot of power into a tiny bundle to give your users the best experience. The entire @form-observer/core library is only 3.2kb minified + gzipped. (If you choose to use a JS Framework Integration instead, then the total bundle size is only 3.5kb minified + gzipped.)
  • Simple and Familiar API: The Form Observer gives you a clear, easy-to-use API that has a similar feel to the standardized observers, such as the Mutation Observer and the Intersection Observer.
  • Framework Agnostic: You can easily use this tool in a pure-JS application or in the JS framework of your choice. The simple API and great Developer Experience remain the same regardless of the tooling you use.
  • Web Component Support: Because the Form Observer is written with pure JS, it works with Web Components out of the box.
  • Flexible: Without requiring any additional setup, the Form Observer allows you to work with fields dynamically added to (or removed from) your forms, fields externally associated with your forms, and more.
  • Easily Extendable: If you have a set of sophisticated form logic that you'd like to reuse, you can extend the Form Observer to encapsulate all of your functionality. We provide a local storage solution and a form validation solution out of the box.

Install

npm install @form-observer/core

Quick Start

Here's an example of how to track the fields that a user has visited:

<!-- HTML -->
<form id="example">
  <h1>Feedback Form</h1>
  <label for="full-name">Full Name</label>
  <input id="full-name" name="full-name" type="text" required />

  <label for="rating">Rating</label>
  <select id="rating" name="rating" required>
    <option value="" selected disabled>Please Choose a Rating</option>
    <option value="horrible">Horrible</option>
    <option value="okay">Okay</option>
    <option value="great">Great</option>
  </select>
</form>

<label for="comments">Additional Comments</label>
<textarea id="comments" name="comments" form="example"></textarea>

<button type="submit" form="example">Submit</button>
/* JavaScript */
import { FormObserver } from "@form-observer/core";
// or import FormObserver from "@form-observer/core/FormObserver";

const form = document.querySelector("form");
const observer = new FormObserver("focusout", (event) => event.target.setAttribute("data-visited", String(true)));
observer.observe(form);
// Important: Remember to call `observer.disconnect` or `observer.unobserve` when observer is no longer being used.

form.addEventListener("submit", handleSubmit);

function handleSubmit(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const visitedFields = Array.from(form.elements).filter((e) => e.hasAttribute("data-visited"));
  // Do something with visited fields...
}

Of course, you can use the Form Observer just as easily in JS Frameworks too

Svelte

<form id="example" bind:this={form} on:submit={handleSubmit}>
  <!-- Internal Fields -->
</form>

<!-- External Fields -->

<script>
  import { onMount } from "svelte";
  import { FormObserver } from "@form-observer/core";
  // or import FormObserver from "@form-observer/core/FormObserver";

  let form;
  const observer = new FormObserver("focusout", (event) => event.target.setAttribute("data-visited", String(true)));
  onMount(() => {
    observer.observe(form);
    return () => observer.disconnect();
  });

  function handleSubmit(event) {
    event.preventDefault();
    const visitedFields = Array.from(event.currentTarget.elements).filter((e) => e.hasAttribute("data-visited"));
    // Do something with visited fields...
  }
</script>

React

import { useEffect, useRef } from "react";
import { FormObserver } from "@form-observer/core";
// or import FormObserver from "@form-observer/core/FormObserver";

function MyForm() {
  // Watch Form Fields
  const form = useRef(null);
  useEffect(() => {
    const observer = new FormObserver("focusout", (event) => event.target.setAttribute("data-visited", String(true)));

    observer.observe(form.current);
    return () => observer.disconnect();
  }, []);

  // Submit Handler
  function handleSubmit(event) {
    event.preventDefault();
    const visitedFields = Array.from(event.currentTarget.elements).filter((e) => e.hasAttribute("data-visited"));
    // Do something with visited fields...
  }

  return (
    <>
      <form id="example" ref={form} onSubmit={handleSubmit}>
        {/* Internal Fields */}
      </form>

      {/* External Fields */}
    </>
  );
}

Interested in learning more? Check out our documentation. A great place to start would be our docs for the Form Observer API or our guides for common use cases.

Too eager to bother with documentation? Feel free to play with our library on StackBlitz or in your own application! All of our tools have detailed JSDocs, so you should be able to learn all that you need to get started from within your IDE.

Solutions to Common Problems

Two common problems that developers need to solve for their complex web forms are:

  1. Storing a user's form progress in localStorage
  2. Validating a form's fields as a user interacts with them

Our library provides solutions for these problems out of the box.

localStorage Solution

/* JavaScript */
import { FormStorageObserver } from "@form-observer/core";
// or import FormStorageObserver from "@form-observer/core/FormStorageObserver";

const form = document.querySelector("form");
const observer = new FormStorageObserver("change");
observer.observe(form);
// Important: Remember to call `observer.disconnect` or `observer.unobserve` when observer is no longer being used.

form.addEventListener("submit", handleSubmit);

function handleSubmit(event) {
  event.preventDefault();
  FormStorageObserver.clear(form); // User no longer needs their progress saved after a form submission
}

Notice that the code required to get the localStorage feature up and running is almost exactly the same as the code that we showed in the Quick Start. All that we did was switch to a feature-focused version of the FormObserver. We also setup an event handler to clear any obsolete localStorage data when the form is submitted.

There's even more that the FormStorageObserver can do. Check out our FormStorageObserver documentation for additional details.

Form Validation Solution

/* JavaScript */
import { FormValidityObserver } from "@form-observer/core";
// or import FormValidityObserver from "@form-observer/core/FormValidityObserver";

const form = document.querySelector("form");
form.noValidate = true;

const observer = new FormValidityObserver("focusout");
observer.observe(form);
// Important: Remember to call `observer.disconnect` or `observer.unobserve` when observer is no longer being used.

form.addEventListener("submit", handleSubmit);

function handleSubmit(event) {
  event.preventDefault();
  const success = observer.validateFields({ focus: true });

  if (success) {
    // Submit data to server
  }
}

Again, notice that the code required to get the form validation feature up and running is very similar to the code that we showed in the Quick Start. The main thing that we did here was switch to a feature-focused version of the FormObserver. We also leveraged some of the validation-specific methods that exist uniquely on the FormValidityObserver.

If you want to use accessible error messages instead of the browser's native error bubbles, you'll have to make some slight edits to your markup. But these are the edits that you'd already be making to your markup anyway if you wanted to use accessible errors.

<!-- HTML -->
<form id="example">
  <h1>Feedback Form</h1>
  <label for="full-name">Full Name</label>
  <input id="full-name" name="full-name" type="text" required aria-describedby="full-name-error" />
  <!-- Add accessible error container here -->
  <div id="full-name-error"></div>

  <label for="rating">Rating</label>
  <select id="rating" name="rating" required aria-describedby="rating-error">
    <option value="" selected disabled>Please Choose a Rating</option>
    <option value="horrible">Horrible</option>
    <option value="okay">Okay</option>
    <option value="great">Great</option>
  </select>
  <!-- And Here -->
  <div id="rating-error"></div>
</form>

<label for="comments">Additional Comments</label>
<textarea id="comments" name="comments" form="example"></textarea>

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

All that we had to do was add aria-describedby attributes that pointed to accessible error message containers for our form fields.

There's much, much more that the FormValidityObserver can do. Check out our FormValidityObserver documentation for additional details.

JS Framework Integrations

Just as there isn't much of a benefit to wrapping the MutationObserver or the IntersectionObserver in a framework-specific package, we don't believe that there's any significant benefit to wrapping the FormObserver or the FormStorageObserver in a framework-specific package. These tools plug-and-play directly into any web application with ease -- whether the application uses pure JS or a JS framework. Consequently, we currently do not provide framework-specific wrappers for most of our observers.

That said, we do provide framework-specific wrappers for the FormValidityObserver. These wrappers technically aren't necessary since the full power of the FormValidityObserver is available to you in any JS framework as is. However, the big selling point of these wrappers is that they take advantage of the features in your framework (particularly, features like props spreading) to reduce the amount of code that you need to write to leverage the FormValidityObserver's advanced features; so it's worth considering using them. We currently provide FormValidityObserver wrappers for the following frameworks:

  • Svelte (@form-observer/svelte)
  • React (@form-observer/react)
  • Vue (@form-observer/vue)
  • Solid (@form-observer/solid)
  • Lit (@form-observer/lit)
  • Preact (@form-observer/preact)

For your convenience, these libraries re-export the tools provided by @form-observer/core -- allowing you to consolidate your imports. For instance, you can import the FormObserver, the FormStorageObserver, the FormValidityObserver, and the Svelte-specific version of the FormValidityObserver all from @form-observer/svelte if you like.

To learn more about how these wrappers minimize your code, see our general documentation on framework integrations.

Live Examples of the FormValidityObserver on StackBlitz:

form-observer's People

Contributors

itenthusiasm avatar

Stargazers

 avatar  avatar Daniel Holmes avatar Zach avatar Julian Cataldo avatar Zafar Ansari avatar marijan milicevic avatar

Watchers

 avatar

form-observer's Issues

Potential Feature: Add a `revalidateOn` Option to the `FormValidityObserver`

Motivation

Most form validation libraries will provide developers with options for revalidating a field that has already been validated at least once.

How It Works

Consider a situation where a developer has asked a form validation utility to validate fields onfocusout (the bubbling version of onblur) and to revalidate them oninput. Let's say that an email field is in the form and is required. If a user visits the field and then leaves it (focusout) without entering anything, then the field will be marked as invalid, and an error message like "Email is required" will be displayed.

The user realizes that they cannot ignore the field. So they return to it and start entering a value. Because this field was already validated at least once, the revalidation feature kicks in. In our case, revalidation occurs oninput; so as the user starts typing in a value, the error message gets updated. If the user enters "e", then the error message will immediately change to "Email is invalid" because the field was revalidated oninput. This error will continue to be displayed until the user provides a valid email address. Note that because revalidation is now happening oninput, the user will be able to see that their field is correct before they leave it (i.e., "blur" it). Note that validation will still occur onfocusout. However, this won't cause a noticeable difference anymore because the field will have been validated on its last input event before the field is blurred.

Sometime later, the user might choose to be silly and modify the email field to something invalid again. As they do so, the error messages will reemerge oninput. Remember: Because this field was already validated at least once, the revalidation feature is active. So even if the user makes the field valid, it will still be validated oninput from this point onwards.

Now let's say that the user has moved on to the confirm-email field. They've learned their lesson with the email field and don't have any interest in generating any error messages. They start to type the letter e; but because the field has not yet been validated (automatically or manually), no error message pops up. They perfectly supply a valid email with a value that matches what's seen in the other email field. So when they leave the confirm-email field, no error message pops up.

This means that the user was able to interact with the confirm-email field without ever seeing an error message for it. However, once the user leaves the field, it will be validated (because the focusout event will have triggered). Consequently, if the user returns to the field and makes it invalid, then they will see error messages for that field.

Difficulty of Implementation

Moderate

The effort certainly shouldn't be significant. But the effort isn't trivial either. (The effort might be "easy", if that were a value between "trivial" and "moderate".)

Usage

From the standpoint of a developer, the usage would be simple and would look something like the following:

const observer = new FormValidityObserver("focusout", { revalidateOn: "input" });

Other Notes

  • Obviously, whatever solution that we come up with should be performant. (At the very least, it should not harm performance.)
  • People could debate whether revalidation should kick in after a field is validated for the first time or after a field is marked as invalid for the first time. The former is probably more intuitive to users.
  • People could debate whether revalidation should be disabled after a field is made valid. My initial intuition is that it is less intuitive for revalidation to be disabled after a field is made valid. (For instance, someone may wonder why a field started to be validated oninput and then suddenly stopped being validated oninput.)
  • Browsers already seem to have some kind of "revalidation behavior". (At least, Chrome does.) When a form field's error message is reported by the browser, the browser will continue to show updated error messages oninput until the field becomes valid. Therefore, the revalidate feature for the FormValidityObserver is arguably an enhancement over what the browser already has.
  • We don't really need to support revalidating onsubmit. The validateFields method is already exposed to developers for use in their submission handlers. They can run validation there instead of relying on a configuration option.
  • If we provide a default value for revalidateOn, it should probably be either input or nothing. I'm leaning towards nothing, as that seems more intuitive. After all, revalidateOn may not even be necessary if validation is done onfocusout or oninput.
  • If possible, the revalidateOn property (or whatever we name it) should probably prevent developers from supplying redundant events. In other words, the following should be forbidden (at least by TypeScript):
const observer = new FormValidityObserver("input", { revalidateOn: "input" });

This would be wasteful and redundant. Therefore, it should be forbidden. (We won't go as far as throwing errors at runtime, though. That's also arguably a waste, though not a significant one.)

Potential Feature: Add a Way to Skip Browser Validation with the `FormValidityObserver`

Motivation

Currently, tools like Zod are very popular for server-side validation in JavaScript applications. Because the tool is not restricted to the backend, many people are also using it on the frontend to validate their form data. It is already possible to use Zod with the FormValidityObserver. But we'd like to make the developer experience a little more streamlined for developers working on full-stack JavaScript applications.

The Problem

Consider a scenario where a developer is using a simple Zod Schema to validate a user signup form in a Remix application. (Remix is a "full-stack React" framework superior to Next.js.) They might have a schema that looks something like this:

import { z } from "zod";

/** Marks empty strings from `FormData` values as `undefined` */
function nonEmptyString<T extends z.ZodTypeAny>(schema: T) {
  return z.preprocess((v) => (v === "" ? undefined : v), schema);
}

const schema = z.object({
  username: nonEmptyString(
    z.string({ required_error: "Username is required" }).min(5, "Minimum length is 5 characters")
  ),
  email: nonEmptyString(z.string({ required_error: "Email is required" }).email("Email is not valid")),
  // Other fields ...
});

A simple Remix Action that validates a form's data might look like this:

import { json } from "@remix-run/node";
import type { ActionFunction } from "@remix-run/node";
import { z } from "zod";

// Zod Schema setup omitted for brevity ...

// Note: We've excluded a success response for brevity
export const action = (async ({ request }) => {
  const formData = Object.fromEntries(await request.formData());
  const result = schema.safeParse(formData);

  if (result.error) {
    return json(result.error.flatten());
  }
}) satisfies ActionFunction;

That's just about everything we need on the backend. On the frontend, a simple usage of the FormValidityObserver in Remix might look something like this:

import { json } from "@remix-run/node";
import type { ActionFunction } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
import { useState, useEffect, useMemo } from "react";
import { createFormValidityObserver } from "@form-observer/react";
import { z } from "zod";

// Setup for the backend omitted for brevity ...

type FieldNames = keyof (typeof schema)["shape"];


// Note: We're omitting the definition for a `handleSubmit` function here to make the problem more obvious.
export default function SignupForm() {
  const serverErrors = useActionData<typeof action>();
  const [errors, setErrors] = useState(serverErrors);
  useEffect(() => setErrors(serverErrors), [serverErrors]);

  const { autoObserve } = useMemo(() => {
    return createFormValidityObserver("input", {
      renderByDefault: true,
      renderer(errorContainer, errorMessage) {
        const name = errorContainer.id.replace(/-error$/, "") as FieldNames;

        setErrors((e) =>
          e
            ? { ...e, fieldErrors: { ...e.fieldErrors, [name]: errorMessage } }
            : { formErrors: [], fieldErrors: { [name]: errorMessage } }
        );
      },
    });
  }, []);

  return (
    <Form ref={useMemo(autoObserve, [autoObserve])} method="post">
      <label htmlFor="username">Username</label>
      <input id="username" name="username" type="text" minLength={5} required aria-describedby="username-error" />
      <div id="username-error" role="alert">
        {errors?.fieldErrors.username}
      </div>

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required aria-describedby="email-error" />
      <div id="email-error" role="alert">
        {errors?.fieldErrors.email}
      </div>

      {/* Other Fields ... */}
      <button type="submit">Submit</button>
    </Form>
  );
}

Although this code is functional, it results in an inconsistent user experience. When a user submits the form, they'll get Zod's error messages for the various form fields. But when a user interacts with the form's fields, then they'll get the browser's error messages instead. This isn't the end of the world, but it's certainly odd and undesirable. (We don't have a submit handler that enforces client-side validation yet. This is intentional to show the issue that we're describing.)

So, to bring in some consistency, we can try to add Zod to the frontend...

// Add Zod Validation to `createFormValidityObserver` configuration
const { autoObserve } = useMemo(() => {
  return createFormValidityObserver("input", {
    renderByDefault: true,
    renderer(errorContainer, errorMessage) {
      const name = errorContainer.id.replace(/-error$/, "") as FieldNames;

      setErrors((e) =>
        e
          ? { ...e, fieldErrors: { ...e.fieldErrors, [name]: errorMessage } }
          : { formErrors: [], fieldErrors: { [name]: errorMessage } }
      );
    },
    defaultErrors: {
      validate(field: HTMLInputElement) {
        const result = schema.shape[field.name as FieldNames].safeParse(field.value);
        if (result.success) return;
        return result.error.issues[0].message;
      },
    },
  });
}, []);

But you'll notice that this change actually doesn't do anything. Why? Because the browser's validation is always run before custom validation functions. So the browser's error messages are still displayed instead of Zod's on the client side. This means that we still have inconsistency between the displayed client errors and the displayed server errors.

Workarounds

There are 2 [undesirable] workarounds for this problem.

1) Omit Validation Attributes from the Form Fields

One option is to remove any attributes that would cause the browser to attempt form field validation. This would cause Zod to be responsible for all error messages displayed to the client.

export default function SignupForm() {
  // Setup ...

  return (
    <Form ref={useMemo(autoObserve, [autoObserve])} method="post">
      <label htmlFor="username">Username</label>
      <input id="username" name="username" type="text" aria-describedby="username-error" />
      <div id="username-error" role="alert">
        {errors?.fieldErrors.username}
      </div>

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="text" aria-describedby="email-error" />
      <div id="email-error" role="alert">
        {errors?.fieldErrors.email}
      </div>

      {/* Other Fields ... */}
      <button type="submit">Submit</button>
    </Form>
  );
}

You'll notice that here, all validation attributes have been removed from our form controls. Additionally, the [type="email"] form control has been changed to [type="text"] to prevent the browser from trying to validate it as an email field. Now the error messages that get displayed on the client side are the exact same as the messages that get returned from the server side.

However, with this new implementation, there is no longer any client-side validation when JavaScript is unavailable. Since users without JavaScript can still submit their forms to our server, our server can still render a new page for them that has the error messages. That's good! However, this increases the chattiness between the client and the server. This also implies that the overall amount of time that the user will spend to submit a successful form will be larger (assuming that they don't perfectly fill it out the first time).

2) Duplicate Error Message Information on the Client Side

Another option is to configure the error messages on the client side to match the error messages on the server side. In addition to synchronizing our client-side errors with our server-side errors, this approach will allow us to keep our validation attributes in our form. This means that users without JavaScript will still be able to see some error messages without having to submit anything to server.

export default function SignupForm() {
  // Other Setup ...

  const { autoObserve, configure } = useMemo(() => {
    return createFormValidityObserver("input", {
      /* Configuration Options without Zod */
    });
  }, []);

  return (
    <Form ref={useMemo(autoObserve, [autoObserve])} method="post">
      <label htmlFor="username">Username</label>
      <input
        id="username"
        type="text"
        aria-describedby="username-error"
        {...configure("username", {
          required: "Username is required",
          minlength: { value: 5, message: "Minimum length is 5 characters" },
        })}
      />
      <div id="username-error" role="alert">
        {errors?.fieldErrors.username}
      </div>

      <label htmlFor="email">Email</label>
      <input
        id="email"
        aria-describedby="email-error"
        {...configure("email", {
          required: "Email is required",
          type: { value: "email", message: "Email is not valid" },
        })}
      />
      <div id="email-error" role="alert">
        {errors?.fieldErrors.email}
      </div>

      {/* Other Fields ... */}
      <button type="submit">Submit</button>
    </Form>
  );
}

Great! We have everything that we need now! Now there are no inconsistencies between the client/server (when JS is available), and users without JS can still get form errors without making too many requests (if any) to our server.

However, this approach is more verbose. You'll notice that now we have to use the configure function to tell the browser which error messages to use when field validation fails. More importantly, we have to duplicate the error messages between our client an our server. For example, the string "Email is required" is written once for our schema and another time for our configure("email", /* ... */) call. To deduplicate our error messages, we could create a local errorMessages object that both the schema and the configure() calls could use. But this causes the boilerplate in our file to get a little larger.

The Solution

The ideal solution to this problem is to provide a way for users to skip the browser's validation without having to remove the validation attributes from their form controls.

const { autoObserve, configure } = useMemo(() => {
  return createFormValidityObserver("input", {
    skipBrowserValidation: true,
    renderByDefault: true,
    renderer(errorContainer, errorMessage) {
      const name = errorContainer.id.replace(/-error$/, "") as FieldNames;

      setErrors((e) =>
        e
          ? { ...e, fieldErrors: { ...e.fieldErrors, [name]: errorMessage } }
          : { formErrors: [], fieldErrors: { [name]: errorMessage } }
      );
    },
    defaultErrors: {
      validate(field: HTMLInputElement) {
        const result = schema.shape[field.name as FieldNames].safeParse(field.value);
        if (result.success) return;
        return result.error.issues[0].message;
      },
    },
  });
}, []);

This option would delegate all validation logic to the custom validation function. (More accurately, it makes the custom validation function the only "agent" that can update the error messages displayed in the UI.) But it would not require developers to remove the validation attributes from their form fields (meaning that users without JavaScript still get some client-side validation). So developers would get to keep their markup small -- just like it was in the beginning of our example:

<Form ref={useMemo(autoObserve, [autoObserve])} method="post">
  <label htmlFor="username">Username</label>
  <input id="username" name="username" type="text" minLength={5} required aria-describedby="username-error" />
  <div id="username-error" role="alert">
    {errors?.fieldErrors.username}
  </div>

  <label htmlFor="email">Email</label>
  <input id="email" name="email" type="email" required aria-describedby="email-error" />
  <div id="email-error" role="alert">
    {errors?.fieldErrors.email}
  </div>

  {/* Other Fields ... */}
  <button type="submit">Submit</button>
</Form>

Counterarguments

There are three counter arguments to the solution provided above.

1) The error messages will still be inconsistent...

If the desire is to provide client-side validation for users who lack JavaScript without requiring them to interact with the server, then the concern of "inconsistent error messages" inevitably appears again. As of today, browsers do not provide a way to override their native error messages without JavaScript. Consequently, with this solution, there will still be a set of users who see "Browser Error Messages" and a different set of users who see "JavaScript/Zod/Server Error Messages".

2) The concern of inconsistent error messages largely goes away if we add a submission handler.

In our example, we didn't have a submission handler. But client-side validation really only makes sense if we're going to block invalid form submissions. In that case, the client should rarely (if ever) encounter situations where they see inconsistent messages between the client and the server -- at least during a given user session. For example, if the server has the error message, "Field is required" and the client has the error message, "Username is required", then once the field is given a value, the server will never respond with a "Field is required" error. Therefore, the user won't see 2 different messages for the same error.

In this case, does inconsistency really matter that much? (It might. But this is still worth asking.) For the solution that we're suggesting, there are already going to be inconsistencies between users who have access to JS and users who do not (because the browser's error messages are not configurable). So again, the inconsistency concern is never fully resolved.

3) It's possible to argue that accessibility is improved if users without JavaScript get error messages from the server instead of getting them from the browser.

A browser can only display an error message for 1 form field at a time. When a user submits an invalid form, the first invalid field displays an error message in a "bubble". After all of the field's errors are resolved, the bubble goes away. But in order for the user to figure out what else is wrong with the form, they have to submit it again. Yes, this can be as easy as pressing the "Enter" key, but it can still be annoying. Additionally, the error bubbles that browsers provide typically won't look as appealing as a custom UI.

By contrast, the server has the ability to return all of the [current] error messages for the form's fields simultaneously. This means that users will know everything else they should fix before resubmitting the form. This user experience is arguably better, and typically prettier (if the error messages have good UI designs). In that case, the first workaround that we showed earlier might be preferable.

Mind you, the server will only respond with current error messages. If an empty email is submitted when the field is required, then the server will first respond with, "Email is required". If an invalid email is supplied afterwards, then the server will respond with, "Email is not valid". On a per-field basis, this is less efficient than what the browser provides out of the box. (Sometimes Zod can help circumvent this limitation, but not always.)

Basically, it's not always clear whether users without JavaScript should be given the browser's validation/messaging experience or the server's validation/messaging experience for forms. Currently, we're stuck with a set of tradeoffs. (Maybe browsers can provide a standard that would resolve this in the future?) And those tradeoffs could pose a reason not to rush to implementing this feature.

Difficulty of Implementation

Trivial

Other Notes

An Overwhelming Number of Options Is Burdensome

Ideally, our FormValidityObserver utility won't have 50 million potential options for its options object. Adding a skipBrowserValidation: boolean option probably isn't the end of the world. But I am starting to get hesitant when it comes to adding additional options. A revalidateOn option is also being considered... (Edit: This revalidateOn option has just recently been added.)

The Client's Validation Is Always a Subset of the Server's Validation

The server ultimately decides all of the validation logic that will be necessary for a given form. The client simply replicates (or reuses) that logic for the sake of user experience. The only way that the client should differ from the server is if it contains only a subset of the server's validation logic. (For instance, the client cannot determine on its own whether a user+password combination is valid. Help from the server will always be needed for this.) Consequently, if developers want to do so, it is appropriate/safe for them to skip the browser's validation and delegate all logic to the Zod schema used on the server.

Sidenote: Constraints Can Be Extracted from Zod Schemas

This is likely something that we won't explore. But if someone was interested in leveraging this information, it's possible:

<input 
  id="username"
  name="username"
  type="text"
  minLength={schema.shape.username._def.schema.minLength}
  required={!schema.shape.username._def.schema.isOptional()}
  aria-describedby="username-error"
/>

This technically relates to our concerns in this Issue since we're interested in minimizing duplication of values, but it's more related to constraints than it is to error messages. And error messages are the greater focus here.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.