Exercise: Proper Email

Exercise 13 Average

Prerequisites for the exercise

  1. React Forms — Controlled Components

Objective

Create an EmailInput component for obtaining and validating an email address.

Description

Email is an extremely useful digital medium to date, even more than half a century since its inception in 1971, being used by all kinds of people — from students in high school to field experts. In that sense, obviously, an email address is a valuable asset to have.

Many web applications and brands online to date rely on gathering email addresses from their users to market them their products, send them timely newsletters, respond back on customer support chats, and whatnot.

Requiring an email address input and then validating it appropriately is, henceforth, quite a necessary thing for these web apps and brands. After all, there is no point in sending an email to an ill-formed email address.

In this exercise, you have to create an EmailInput component that wraps an <input> element to obtain an email address, making sure that the entered value is perfectly valid.

Initially, when the page loads, the input field should have no errors and no special styling — just be a basic input field. An illustration follows:

Email input field's initial view
Email input field's initial view

When the field receives focus, its border must be turned purple:

Input field when it receives focus
Input field when it receives focus

Once a value is entered, it should be processed ONLY when the field loses focus.

At this point, if the value is a valid email address, the input field's border must be turned green, as follows:

Input field when email is valid
Input field when email is valid

Otherwise, the input field's border must be turned red, along with the necessary error message displayed beneath the input field:

Input field when email is invalid
Input field when email is invalid

One of the following two error messages must be shown, depending on whose condition gets fulfilled first:

  • "Email address is required!" Should be shown when the field is left blank.
  • "Email address isn't valid!" Should be shown when the value doesn't stand up to the format of a valid email address.

Reiterating on it, when the input field doesn't have focus, it must either showcase a correct input display, a wrong input display, or a neutral display (i.e. when the field hasn't been focused since the page's load).

But when the field gets focus back, its border must be turned back to the color of its focused state, and the error message hidden, if any.

Something as follows for the example illustrated above:

Input field receiving focus with an invalid value
Input field receiving focus with an invalid value

Letting focus go off the field here should end up with the same response as described previously (that is, give a signal for a correct input or for a wrong one):

Input field when email is invalid
Input field when email is invalid

And that's essentially it!

As for the technical description of EmailInput, it should accept in three main props:

  • name, for the name of the underlying input field.
  • value, for the initial value of the underlying input field. This is optional.
  • label, for the the text to use to label the input field. This is optional. By default, it should be 'Email:'

Besides these two, any other prop should get relayed forward to the input field. As simple as that.

For instance, the following usage of EmailInput should render the input field with a light pink background, since it has the respective CSS style property set on it:

<EmailInput
   name="email"
   value="mail@codeguage.com"
   style={{ backgroundColor: 'lightpink' }}
/>

Note that providing a style prop to EmailInput should NOT override the border styles of the underlying input as long as the given styles don't contain a border property.

If a border property is specified, it should take precedence over our border logic (regardless of whether we are showing a correct, wrong, or neutral input display).

Here's a live, running example for the code show above:

Live Example

Notice how, in spite of the given backgroundColor inline style, the border-coloring logic of EmailInput continues to action in the linked program.

View Solution

New file

Inside the directory you created for this course on React, create a new folder called Exercise-13-Proper-Email and put the .js solution files for this exercise within it.

Solution

Let's start by writing the minimal code that we need in EmailInput — you know, things that are very very basic.

Following is this minimal code:

function EmailInput({
   name,
   value: initialValue = '',
   label = 'Email:',
   ...props
}) {
   const [value, setValue] = useState(initialValue);
   const [error, setError] = useState(null);

   return (
      <label>
         <p class="input-label">{label}</p>
         <input
            type="text"
            className="input-text"
            name={name}
            value={value}
            onChange={e => setValue(e.target.value)}
            {...props}
         />
         {error && <p className="input-error">{error}</p>}
      </label>
   );
}

The <input> element, its preceding label text, and its error message are all styled using CSS classes — .input-text, .input-label, and .input-error, respectively.

With this, let's also define some very basic CSS code for our program:

body, input {
   font-family: 'Arial', sans-serif;
}

.input-text {
   background-color: #f1f1f1;
   padding: 7px 10px;
   display: inline-block;
   font-size: 18px;
   border-radius: 4px;
   border: 2px solid transparent;
   outline: none;
}
.input-text:focus {
   border-color: blueviolet;
}
.input-text--wrong {
   border-color: red;
}
.input-text--correct {
   border-color: green;
}

.input-label {
   font-size: 14px;
   margin-bottom: 10px;
}

.input-error {
   color: red;
   margin: 5px;
   font-size: 12px;
}

The .input-text--correct and .input-text--wrong modifier classes aren't being used currently in the JSX code above but we'll do so shortly below.

Now, let's get to the real business of setting up the invalid email–handling logic in the component, with all its border color transitions.

There are essentially four different cases to handle:

  • When the input isn't touched at all.
  • When a value is input but it's invalid, with a corresponding error message.
  • When a value is input and it's perfectly valid.

So what state values can we use to model each of these? Or do we not require any additional states besides error (as we defined above)?

Well, the latter is right; we don't need any new state apart from error. Let's see how:

  • When the input isn't touched at all, error would be null.
  • When a value is input but it's invalid, with a corresponding error message, error would be a (non-empty) string holding the error message.
  • When a value is input and it's perfectly valid, error would be false.

Notice the distinction we make between the error values null and false.

In programming, null typically means an empty value, thus a null error state means that we don't know whether there is any error or not. In contrast, a false error state means that we are sure that there is no error.

Based on this idea of error, let's advance our EmailInput component's implementation where it makes sense to.

And for now, it makes sense to decide on the set of classes to apply to the <input> element, based on the value of the error state. This is accomplished below:

function EmailInput({
   name,
   value: initialValue = '',
   label = 'Email:',
   ...props
}) {
   const [value, setValue] = useState(initialValue);
   const [error, setError] = useState(null);

   return (
      <label>
         <p class="input-label">{label}</p>
         <input
            type="text"
className={classNames({ 'input-text': true, 'input-text--wrong': error, 'input-text--correct': error === false })}
name={name} value={value} onChange={e => setValue(e.target.value)} {...props} /> {error && <p className="input-error">{error}</p>} </label> ); }

All that is left now is setting up handlers for the focus and blur events on the <input> element. This is what we do up next.

The onFocus handler's definition is going to be quite trivial, as shown below:

function EmailInput({
   name,
   value: initialValue = '',
   label = 'Email:',
   ...props
}) {
   const [value, setValue] = useState(initialValue);
   const [error, setError] = useState(null);

   return (
      <label>
         <p class="input-label">{label}</p>
         <input
            type="text"
            className={classNames({
               'input-text': true,
               'input-text--wrong': error,
               'input-text--correct': error === false
            })}
            name={name}
            value={value}
            onChange={e => setValue(e.target.value)}
onFocus={() => setError(null)}
{...props} /> {error && <p className="input-error">{error}</p>} </label> ); }

The exercise's description clearly states that when the input field obtains focus, its error messages must be cleared up and the same for its border styles. Going with the error state notion we formulated above, this means to update error to null.

So far, so good.

Over to the implementation of the onBlur handler:

function EmailInput({
   name,
   value: initialValue = '',
   label = 'Email:',
   ...props
}) {
   ...

   return (
      <label>
         <p class="input-label">{label}</p>
         <input
            ...
onBlur={e => validateEmail(e.target.value)}
{...props} /> {error && <p className="input-error">{error}</p>} </label> ); }

As can be seen here, the task of handling blur is essentially delegated to a separate function validateEmail(), providing it the value of the input field the moment the event fires.

Let's see what this function is about:

function EmailInput({
   name,
   value: initialValue = '',
   label = 'Email:',
   ...props
}) {
   ...

function validateEmail(value) { if (value === '') { setError('Email address is required!'); } else if (!/[a-zA-Z0-9.]+@[a-zA-Z0-9.]+\.[a-zA-Z0-9.]{2,}/.test(value)) { setError('Invalid email address!'); } else { setError(false); } }
... }
  • If the given value is an empty string, that is when the input field is left blank at the time of the blur, an error is set, reading 'Email address is required!', as prescribed by the exercise's description above.
  • Otherwise, if the given value is not a valid email address, which is checked via a basic regular expression, an error is set, reading 'Invalid email address!'
  • Otherwise, since we know at this point that the entered value is valid as an email address, a false error is set. Recall that this means that the given input is valid.

And that's essentially it.

Altogether, following is the complete definition of EmailInput:

function EmailInput({
   name,
   value: initialValue = '',
   label = 'Email:',
   ...props
}) {
   const [value, setValue] = useState(initialValue);
   const [error, setError] = useState(null);

   function validateEmail(value) {
      if (value === '') {
         setError('Email address is required!');
      }
      else if (!/[a-zA-Z0-9.]+@[a-zA-Z0-9.]+\.[a-zA-Z0-9.]{2,}/.test(value)) {
         setError('Invalid email address!');
      }
      else {
         setError(false);
      }
   }

   return (
      <label>
         <p class="input-label">{label}</p>
         <input
            type="text"
            className={classNames({
               'input-text': true,
               'input-text--wrong': error,
               'input-text--correct': error === false
            })}
            name={name}
            value={value}
            onChange={e => setValue(e.target.value)}
            onFocus={() => setError(null)}
            onBlur={e => validateEmail(e.target.value)}
            {...props}
         />
         {error && <p className="input-error">{error}</p>}
      </label>
   );
}

Let's test it a bit differently than done in the exercise's description above:

function App() {
   return (
      <EmailInput
         label="New email"
         name="email"
         placeholder="e.g. alice@example.com"
         style={{ backgroundColor: 'azure' }}
      />
   );
}

Live Example

One word: perfect!

This completes this exercise.