Skip to content

Instantly share code, notes, and snippets.

@harrisonmalone
Last active July 4, 2023 22:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save harrisonmalone/14ab4d35f71ae78b8f23274df008aeae to your computer and use it in GitHub Desktop.
Save harrisonmalone/14ab4d35f71ae78b8f23274df008aeae to your computer and use it in GitHub Desktop.
form validation and events lecture for m0119

Lecture ✍️

JS Events

So far we have mostly been using HTML buttons to trigger JS functions, but this is only one event we can use to trigger logic from our JS files. In previous tasks we set up what is called an event listener to detect when the button was clicked. We passed a callback as a function that gets called when event is triggered. Being able to pass functions as parameters - the same way as we can with variables - is one of the major strengths of the JS language.

Event-based programming can be a little tricky to wrap your head around, especially as a beginner. Up until now you have probably been mainly programming in a sequential or imperative way - each line is executed one after another. When we introduce event listeners and callbacks we are determining the functionality we want to happen but we are not actually in control of when this functionality happens - functions aren't called until the button is actually clicked by the user.

Let's create a Coder Academy webapp that allows users to create an account. Create a new register.html file and add the following Bootstrap boilerplate code.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Coder Academy - Register Account</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
    <link rel="stylesheet" type="text/css" href="styles.css" />
  </head>
  <body>
    <div class="container">

    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    <script src="register.js"></script>
  </body>
</html>

Add the following <form> element inside the .container div.

<form id="registration-form" novalidate>
  <div class="form-group">
    <label for="username">Username</label>
    <input type="text" class="form-control" id="username" placeholder="John-Smith" autofocus>
    <div class="invalid-feedback">
      Username is required and must be at least three characters long.
    </div>
  </div>
  <div class="form-group">
    <label for="email">Email address</label>
    <input type="email" class="form-control" id="email" placeholder="john.smith@example.com">
    <div class="invalid-feedback">
      Please provide a valid email address.
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" placeholder="password123">
    <div class="invalid-feedback">
      Password must be at least six characters long and contain at least one special character.
    </div>
  </div>
  <div class="form-group">
    <label for="confirmpassword">Confirm password</label>
    <input type="password" class="form-control" id="confirmpassword" placeholder="password123">
    <div class="invalid-feedback">
      Passwords must match.
    </div>
  </div>
  <div class="form-check">
    <input type="checkbox" class="form-check-input" id="agree">
    <label class="form-check-label" for="agree">I agree to all terms and conditions</label>
    <div class="invalid-feedback">
      Please agree to the terms and conditions.
    </div>
  </div>
  <button type="submit" class="btn btn-danger btn-lg btn-block" id="submit" disabled>Create Account</button>
</form>

This looks quite complicated but really it is just because of the extra divs and classes that are required for Bootstrap forms. Read through each of the attributes and try to notice some patterns.

The form element has a novalidate attribute on it. This tells the browser that we do not want to apply any HTML validation, meaning we want to take on the responsibility of all validation using either JS or some form of server-side validation.

Each input and label is surrounded by an extra div to apply the Bootstrap styling class form-control. There is an extra <div> with the class .invalid-feedback inside each form-group that Bootstrap knows to hide by default. This contains our error message if the input field fails validation.

The username input is focused by default and the button is disabled so we cannot actually submit the form. Let's write some JS to validate our form. Create the register.js file if you have not already done so and add the following line to select all the elements with the .form-control class.

const inputs = document.querySelectorAll('.form-control');

Notice we have used the querySelectorAll() method, rather than just the querySelector(). As mentioned earlier, querySelectorAll() selects a collection of elements that match the CSS rule, whereas querySelector() only selects one - the first one it finds.

This means the inputs variable is a collection or array, rather than a single element. Let's loop over all of them and log them out to the console.

inputs.forEach(input => {
  console.log(input);
});

This will iterate over each element of the collection and allow us to run some code. We can access the current element we are iterating over as input during each loop. Refresh the browser and you should see the four elements printed to the console.

Now replace the console.log statement with the following code.

input.addEventListener('blur', (event) => {
  console.log(event);
});

This will setup what is called an event listener - something that will wait for a particular event to occur and then run some provided code. In this case the event we want to listen for in the blur event, which occurs when an input field that has focus - the cursor is in the box waiting for input - loses focus - the cursor is moved to another input field.

When this blur event occurs JS will execute the callback we passed it as the second parameter. A callback is just a function that is going to run when the event occurs, rather than being called immediately.

So in the above example we will get the event printed to the console every time an input loses focus. Test this by saving the JS file, refreshing the browser and then clicking between the inputs. Each time you click a new input you should get a new FocusEvent printed to the console. This is an event object and it contains all the information that the browser knows about the moment that this event occurred. There is a lot of information that we will probably never use, but it also has very valuable information like which elements lost and gained focus. We won't be using the event for this example, but important to know about for the future.

Great, we are now listening for the blur event on each of our <input> elements. Let's replace the console.log message with a function call to validateForm().

validateForm();

Create the validateForm function and write a console.log message to confirm it is being triggered.

const validateForm = () => {
  console.log('validating form');
}

Now refresh the browser and click between the inputs. You should see the message print to the console each time you blur an input. Great, now we are going to keep our functions nice and small so they can be reused and easily maintained.

Replace the console.log statement with a function call to validateUsername() and implement the following function.

const validateUsername = () => {
  const username = document.querySelector('#username');
  if (username.value === "" || username.value.length < 3) {
    username.classList.remove('is-valid');
    username.classList.add('is-invalid');
  } else {
    username.classList.remove('is-invalid');
    username.classList.add('is-valid');
  }
}

This function selects the username element from the DOM and checks whether its value is empty or less than three characters. The || is the Javascript syntax for or. If you're unfamiliar with boolean expressions and truth tables, you may want to take a quick look at this.

If either of those conditions are true then we will run the first block of code. If neither of them are true, we will run the second block of code. This is simply toggling the .is-valid and .is-invalid classes. These are special Bootstrap form classes that will reveal our error message if validation failed and change the colour of the input's border to match its state. This makes it very easy to communicate to the user what they need to fix before submitting the form.

Next let's implement a validateEmail() function.

const validateEmail = () => {
  const email = document.querySelector('#email');
  const expression = /(?!.*\.{2})^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i;
  const isValid = expression.test(email.value);
  if (!isValid) {
    email.classList.remove('is-valid');
    email.classList.add('is-invalid');
  } else {
    email.classList.remove('is-invalid');
    email.classList.add('is-valid');
  }
}

We have just copied an email regex pattern from the Internet, as it is surprisingly difficult to do properly. Its an ongoing discussion online and a small amount of research will probably reveal that this does not cover all cases, but it is good enough for this exercise. It also confirms that we can use regex to validate our inputs against patterns. This means we can enforce much more complex rules for things like passwords. The expression.test() method returns true if the string matches the pattern and false if it doesn't. We are then using this Boolean in the if statement, along with a negation or not operator - !. The not operator flips the Boolean to the opposite value - true to false or false to true.

So essentially, if the email is not valid we want to set the invalid class, otherwise we want to set the valid class. This is very similar to what we did for validateUsername().

We will be extending this exercise for the challenge so we won't be implementing validation for the other fields just yet. Let's implement the final function that can check whether the is-invalid class exists on any elements within the form. This will allow us to determine whether our user should be able to submit the form or not.

const isFormValid = () => {
  const invalidFields = document.querySelectorAll('.is-invalid');
  const button = document.querySelector('#submit');
  if (invalidFields.length === 0) {
    button.removeAttribute('disabled');
  } else {
    button.setAttribute('disabled', "");
  }
}

Remember querySelectorAll() returns a collection (even if that collection is empty). Therefore, if it is empty there are no invalid inputs in the DOM, so the form is valid. By removing the disabled attribute from the submit button we are visually communicating to our user that they are now able to submit the form. Let's update validateForm() to call our new functions and validate the form.

const validateForm = () => {
  validateUsername();
  validateEmail();
  // validate other inputs
  isFormValid();
}

Save all files and refresh the browser and it should now be enforcing that the username and email fields are correct. Validation should run every time an input field loses focus and when all validation rules are met, the button should be enabled.

Time for some challenges!

Activity

Challenges

  1. Implement a new function to validate the password and confirm password fields. Validate that:
  • values in both fields match
  • password contains at least six characters
  • At least one character is the following - @, $, % or &
  1. Implement a function to check that the agree checkbox has been ticked. You will need to add an additional event listener that is triggered when the checkbox is ticked.

Optional challenges

  • Implement custom error messages for each username and password condition:
    • username must be more than three characters long
    • username must not be empty
    • password must be at least six characters long
    • password must contain at least one of the following characters - @, $, % or &
    • password and confirm password fields must match
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment