Adam
Adam

Reputation: 2552

Validate email address within autocomplete field

I'm using Material UI to create an autocomplete field with multiple inputs which allows the user to either select an existing email address, or enter in their own. For example, something like this:

enter image description here

Right now, the user can enter in their email addresses successfully, or select one from the dropdown menu - essentially, the same as the linked example above.

However, I am now trying to work on the email validation so that a couple of things happen:

  1. Upon hitting the "enter" key, I check whether the email is valid or not. If it's not, an error message should be displayed to the user and the entered email address is not added to the running list
  2. Whenever there is an error, any subsequent action (either backspace, typing, click "X" etc), should remove the error message

As of now, I am able to validate the email address as in point 1 above, but I am not sure how to stop the value from being added to the list when the user hits the "enter" key. In addition, to remove the error message, I am only able to do so when the user types or removes additional characters (i.e. via the onChange method). However, if the user interacts with the Autocomplete component (for example, clicks "X" to remove the email address), the error stills shows.

This is what I have so far:

import React, { useState } from "react";
import Chip from "@mui/material/Chip";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import Stack from "@mui/material/Stack";

export default function Tags() {
  const [emails, setEmails] = useState([]);
  const [currValue, setCurrValue] = useState(undefined);
  const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  const [error, setError] = useState(false);

  const emailAddresses = [
    { title: "[email protected]" },
    { title: "[email protected]" },
    { title: "[email protected]" }
  ];

  const handleValidation = (e) => {
    // check if the user has hit the "enter" key (which is code "13")
    if (e.keyCode === 13 && !regex.test(e.target.value)) {
      setError(true);
    }
  };

  const handleChange = (e) => {
    // anytime the user makes a modification, remove any errors
    setError(false);
    setCurrValue(e.target.value);
  };

  console.log("emails", emails);

  return (
    <Stack spacing={3} sx={{ width: 500 }}>
      <Autocomplete
        multiple
        onChange={(event, value) => setEmails(value)}
        id="tags-filled"
        options={emailAddresses.map((option) => option.title)}
        freeSolo
        renderTags={(value: readonly string[], getTagProps) =>
          value.map((option: string, index: number) => (
            <Chip
              variant="outlined"
              label={option}
              {...getTagProps({ index })}
            />
          ))
        }
        renderInput={(params) => (
          <TextField
            {...params}
            variant="filled"
            label="Email Addresses"
            placeholder="Favorites"
            type="email"
            value={currValue}
            onChange={handleChange}
            onKeyDown={handleValidation}
            error={error}
            helperText={error && "Please enter a valid email address"}
          />
        )}
      />
    </Stack>
  );
}

The example on Code Sandbox is here: https://codesandbox.io/s/tags-material-demo-forked-5l7ovu?file=/demo.tsx

Note: I'm not entirely sure how to provide the replicable code on Stack Overflow so I apologise in advance for linking my code to Code Sandbox instead.

Upvotes: 1

Views: 2491

Answers (1)

A G
A G

Reputation: 22587

You need to use a controlled autocomplete. In onChange we need to do -

  1. If there is any invalid email, remove it from the array & update state to valid emails. (Chips)

  2. We still need to show the invalid email as text (not a chip), for this we can set inputValue. (Text)

  3. Set or remove error.

function onChange(e, value) {
    // error
    const errorEmail = value.find((email) => !regex.test(email));
    if (errorEmail) {
      // set value displayed in the textbox
      setInputValue(errorEmail);
      setError(true);
    } else {
      setError(false);
    }
    // Update state, only valid emails
    setSelected(value.filter((email) => regex.test(email)));
  }

As it controlled, we also need to handle chip's onDelete & update state. Full code & working codesandbox

export default function Tags() {
  const [selected, setSelected] = useState([]);
  const [inputValue, setInputValue] = useState("");
  const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  const [error, setError] = useState(false);

  const emailAddresses = [
    { title: "[email protected]" },
    { title: "[email protected]" },
    { title: "[email protected]" }
  ];

  function onChange(e, value) {
    // error
    const errorEmail = value.find((email) => !regex.test(email));
    if (errorEmail) {
      // set value displayed in the textbox
      setInputValue(errorEmail);
      setError(true);
    } else {
      setError(false);
    }
    // Update state
    setSelected(value.filter((email) => regex.test(email)));
  }

  function onDelete(value) {
    setSelected(selected.filter((e) => e !== value));
  }

  function onInputChange(e, newValue) {
    setInputValue(newValue);
  }

  return (
    <Stack spacing={3} sx={{ width: 500 }}>
      <Autocomplete
        multiple
        onChange={onChange}
        id="tags-filled"
        value={selected}
        inputValue={inputValue}
        onInputChange={onInputChange}
        options={emailAddresses.map((option) => option.title)}
        freeSolo
        renderTags={(value: readonly string[], getTagProps) =>
          value.map((option: string, index: number) => (
            <Chip
              variant="outlined"
              label={option}
              {...getTagProps({ index })}
              onDelete={() => onDelete(option)} //delete
            />
          ))
        }
        renderInput={(params) => (
          <TextField
            ....
          />
        )}
      />
    </Stack>
  );
}

Upvotes: 4

Related Questions