Lesson 12: Forms - strvcom/frontend-academy-2022 GitHub Wiki

Speaker: Lukas Polak

Resources


Lesson notes

FORMS IN HTML & REACT

HTML

  • We need to use form controls like input, textarea, select, button
  • form tag needs to have method and action attributes
  • State (values) is managed by the DOM
Example
<form action="/some-url" method="GET">
  <label for="email">Email</label>
  <input type="email" id="email" name="email" />
  <label for="password">Password</label>
  <input type="password" id="password" name="password" />
  <!-- We do not need to specify `type=submit` as it is an default value -->
  <!-- We can also use `<input type="submit">` -->
  <button>Submit</button>
</form>

CodeSandbox

Enhanced Example (more attributes)
<body>
  <form action="/some-url" method="GET">
    <label for="email">Email</label>
    <input type="email" id="email" name="email" />
    <br />
    <label for="password">Pass</label>
    <input type="password" id="password" name="password" />
  </form>
  <hr />
  <form action="/some-url" method="GET">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" />
    <br />
    <label for="number">Age</label>
    <input type="number" id="number" name="number" />
    <br />
    <label for="tel">Tel</label>
    <input type="tel" id="tel" name="tel" />
  </form>
</body>

CodeSandbox - enhancement

REACT

  • form tag needs to have onSubmit callback instead of method and action attributes
  • For easier "data manipulation", we can use the FormData constructor to create key/value pairs representing form fields and their values.
Example
export default function App() {
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    // prevent page reload on form submission
    e.preventDefault();

    const formData = new FormData(e.target as HTMLFormElement);
    const email = formData.get("email");
    const password = formData.get("password");

    alert(`email: ${email}\npassword: ${password}`);
  };

  return (
    // instead of `action` and `method` attributes we will use only `onSubmit` prop
    <form onSubmit={handleSubmit}>
      {/* change `for` to `htmlFor` */}
      <label htmlFor="email">Email</label>
      <input type="email" id="email" name="email" />
      {/* change `for` to `htmlFor` */}
      <label htmlFor="email">Password</label>
      <input type="password" id="password" name="password" />
      <button>Submit</button>
    </form>
  );
}

CodeSandbox


CONTROLLED VS. UNCONTROLLED COMPONENTS

UNCONTROLLED

  • Local state is managed by DOM
  • Less configuration & less flexible
  • <input type="file" /> is always uncontrolled as value cannot be set programmatically
  • To specify initial value we can use defaultValue prop
  • The term “uncontrolled” is not tightly coupled with the form elements!

CONTROLLED

  • The state is driven by props; React manages the state
  • more configuration & more flexibility
  • value and onChange props are required
  • React team recommends using controlled components
Example
export default function App() {
  // initialize states for the input values
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    alert(`email: ${email}\npassword: ${password}`);
  };

  // initialize `onChange` callbacks
  const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
    setPassword(e.target.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        // pass `value` and `onChange` props
        value={email}
        onChange={handleEmailChange}
      />
      <label htmlFor="email">Password</label>
      <input
        type="password"
        id="password"
        name="password"
        // pass `value` and `onChange` props
        value={password}
        onChange={handlePasswordChange}
      />
      <button>Submit</button>
    </form>
  );
}

CodeSandbox

Enhanced Example (generic state)
export default function App() {
  // initialize states for the input values
  const [values, setValues] = useState<{ email?: string; password?: string }>(
    {}
  );

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    alert(`email: ${values?.email}\npassword: ${values?.password}`);
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setValues({ ...values, [e.target.name]: e.target.value });
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        value={values.email}
        onChange={handleChange}
      />
      <label htmlFor="email">Password</label>
      <input
        type="password"
        id="password"
        name="password"
        value={values.password}
        onChange={handleChange}
      />
      <button>Submit</button>
    </form>
  );
}

CodeSandbox - enhancement


SYNTHETIC EVENTS

  • React uses SyntheticEvent, which is a wrapper around the browser's native event
  • It works identically across all browsers
  • It has the same interface as the browser's native event
  • It has a better performance
  • To get a native browser event, you can use the nativeEvent attribute

FORM VALIDATIONS

BUILT-IN

  • Every major browser has native components that show validation messages
  • The same validation "error" looks different in different browsers.
  • Uses Constraint Validation API in the background
  • Easy to use, but hard to tweak
import { FormEvent } from "react";

export default function App() {
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log("data submitted");
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      {/* add `required` prop */}
      <input type="email" id="email" name="email" required />
      <label htmlFor="email">Password</label>
      {/* add `required` prop */}
      <input type="password" id="password" name="password" required />
      <label htmlFor="string">String</label>
      <input
        type="text"
        id="string"
        name="string"
        // add `required` prop
        required
        // add `pattern` prop
        pattern="[A-Za-z]+"
      />
      <button>Submit</button>
    </form>
  );
}

CodeSandbox

CONSTRAINT VALIDATION API

  • It enables us to check the values that users have entered before submitting the values to the server.
  • Widely supported
  • Harder to configure, but more flexible than with built-in validations UI
  • It doesn't have "bulletproof" validations
  • It doesn't replace the backend validations!
import { FocusEvent, useState } from "react";

export default function App() {
  const [error, setError] = useState("");
  const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
    if (e.target.validity.valid) {
      setError("");
    } else {
      setError(e.target.validationMessage);
    }
  };

  return (
    <form>
      <label htmlFor="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        required
        onBlur={handleBlur}
        // turn off native validation
        formNoValidate
      />
      {error ? <span>{error}</span> : null}
    </form>
  );
}

CodeSandbox


THIRD-PARTY FORM & VALIDATIONS LIBRARIES

  • Quick and easy way to abstract complex logic in forms
  • Reduce repetitive tasks
  • Too much abstraction could obscure things and make customization difficult
  • Increased bundle size

FORM LIBRARIES

VALIDATION LIBRARIES


WHEN TO USE THIRD-PARTY SOLUTIONS

BUILT-IN

  • You have only one form in the app
  • You have a simple form logic
  • You want to challenge yourself

THIRD-PARTY

  • You have lots of forms
  • You have a complex validation logic
  • You do not have time to tinker own solution

UX SUGGESTIONS

  • Show errors as soon as possible but in a reasonable way
  • Use proper semantics
  • Show loading state while submission
  • Disable button while loading state is present
  • Use proper attributes (tel email autocomplete) to improve the experience (not only) for mobile users
  • Do not disable the button when validation is depended on the submission
  • <input type="date" /> and <input type="time" /> are supported in Safari since version 14.1 (September 13, 2021)
    • Chrome has supported that since version 20 (June 26, 2012)
    • IE has no support, and IE is not dead yet and won't be dead for a few years now.

RESOURCES

⚠️ **GitHub.com Fallback** ⚠️