How I built a password toggle script in vanilla JavaScript

When I went through the Vanilla JS Academy, my first project was a script that allows users to toggle the visibility of a password field. I then refactored it to support multiple fields, and finally multiple forms. Here’s how I did it.

  1. A single field
  2. Multiple fields
  3. Multiple fields across multiple forms

A single field

Here’s the markup I started with:

<form>
  <div>
    <label for="username">Username</label>
    <input id="username" type="text" name="username">
  </div>

  <div>
    <label for="password">Password</label>
    <input id="password" type="password" name="password">
  </div>

  <div>
    <label for="show-password">
      <input id="show-password" type="checkbox">
      Show password
    </label>
  </div>

  <div>
    <button type="submit">Log In</button>
  </div>
</form>

To kick things off, I created an immediately-invoked function expression (IIFE) to contain the rest of my code:

;(function(d) {

  "use strict";

  // Rest of code...

})(document);

This does a few things. First and foremost, it lets me keep my code out of the global scope to avoid naming conflicts with other scripts. For instance, if another script also used a variable called toggle, we’d run into trouble. This prevents that headache.

You’ll also notice I’ve declared d as a parameter of the IIFE, and passed in the document as my argument. This isn’t necessary, but it’s a nice little way of minifying commonly-used variables, such as globals. It lets me refer to the document as just d for short.

The "use strict" statement opts the function into strict mode, first introduced in ES5. As explained in the MDN Web Docs:

Strict mode makes several changes to normal JavaScript semantics:

  1. Eliminates some JavaScript silent errors by changing them to throw errors.
  2. Fixes mistakes that make it difficult for JavaScript engines to perform optimizations: strict mode code can sometimes be made to run faster than identical code that’s not strict mode.
  3. Prohibits some syntax likely to be defined in future versions of ECMAScript.

This isn’t required, but it can make the script a little more bulletproof, so I like to do it. The same goes for the leading semicolon on my IIFE; it can prevent errors when concatenating scripts.

Inside my IIFE, I declared my variables:

// Get the toggle and password elements
var toggle = d.querySelector("#show-password");
var password = d.querySelector("#password");

Here, I simply got a reference to two elements in my HTML. The toggle variable refers to input#show-password, while the password variable refers to input#password.

I then created a function, togglePassword:

/**
 * Toggle the visibility of a password field
 */
function togglePassword() {
  password.type = this.checked ? "text" : "password";
}

This togglePassword function lets me toggle the type attribute of the password field between text and password. I used this function within an event listener attached to the toggle element, so this refers to the toggle element in this context.

If the toggle is checked, the type attribute of the password element will be set to text (visible); otherwise, it will be set to password (concealed).

If you haven’t seen a ternary operator before, it’s just a shorthand if/else statement, best used for simple conditions such as this. In other words, it’s just a shorter way of writing the following:

if (this.checked) {
  password.type = "text";
} else {
  password.type = "password";
}

Finally, I initialized my script.

At the time of writing, Firefox caches checkbox state. That is, if you activate a checkbox and then reload the page, it will stay checked. Therefore, I am simply checking if the toggle element is checked, and if so, setting the type attribute of the password element to text:

// If the toggle is checked, show the password (Firefox)
if (toggle.checked) password.type = "text";

Last of all, I set up my event listener to execute the togglePassword function when the toggle element fires the change event:

// Toggle the password field when the toggle is used
toggle.addEventListener("change", togglePassword, false);

And that completes the first part of this project. Here’s the complete script:

;(function(d) {

  "use strict";

  //
  // Variables
  //

  // Get the toggle and password elements
  var toggle = d.querySelector("#show-password");
  var password = d.querySelector("#password");


  //
  // Functions
  //

  /**
   * Toggle the visibility of a password field
   */
  function togglePassword() {
    password.type = this.checked ? "text" : "password";
  }


  //
  // Init
  //

  // If the toggle is checked, show the password (Firefox)
  if (toggle.checked) password.type = "text";

  // Toggle the password field when the toggle is used
  toggle.addEventListener("change", togglePassword, false);

})(document);

Multiple fields

To start, I adjusted the HTML a little. This time, when activated, the checkbox should toggle the visibility of both password fields simultaneously:

<form>
  <div>
    <label for="current-password">Current Password</label>
    <input id="current-password" type="password" name="current-password" data-password>
  </div>

  <div>
    <label for="new-password">New Password</label>
    <input id="new-password" type="password" name="new-password" data-password>
  </div>

  <div>
    <label for="show-passwords">
      <input id="show-passwords" type="checkbox" name="show-passwords">
      Show passwords
    </label>
  </div>

  <div>
    <button type="submit">Change Passwords</button>
  </div>
</form>

Once again, I created an IIFE to contain my code:

;(function(d) {

  "use strict";

  // Rest of code...

})(document);

Within my IIFE, I created my variables:

// Get the toggle and password elements
var toggle = d.querySelector("#show-passwords");
var passwords = d.querySelectorAll("[data-password]");

The differences here are:

  1. The ID attribute is now plural (just for accuracy): #show-passwords.
  2. I added a data-password attribute to both password fields. I am using it as my selector within the .querySelectorAll() method to get both fields.

I then created two functions.

First up, the togglePassword function:

/**
 * Toggle the visibility of a password field
 * @param {Node} field The password field
 */
function togglePassword(field) {
  field.type = toggle.checked ? "text" : "password";
}

The togglePassword function is the same as before, except it now has a single parameter: field. This allows me to pass in a field as my argument, which I will do later using the NodeList.prototype.forEach() method.

Next, the toggleAllPasswords function:

/**
 * Toggle all password fields
 */
function toggleAllPasswords() {
  passwords.forEach(togglePassword);
}

The toggleAllPasswords function is mainly for readability, but it calls the togglePassword function for each of the elements stored inside the passwords variable.

This works because the NodeList.prototype.forEach() method automatically passes the current item in the loop into the togglePassword function.

Finally, I initialized my script:

// If the toggle is checked, show both passwords (Firefox)
if (toggle.checked) toggleAllPasswords();

// Toggle both passwords when the checkbox is activated
toggle.addEventListener("change", toggleAllPasswords, false);

This is virtually the same as last time, except I’m using the toggleAllPasswords function instead of toggling just a single field.

Here’s the complete script:

;(function(d) {

  "use strict";

  //
  // Variables
  //

  // Get the toggle and password elements
  var toggle = d.querySelector("#show-passwords");
  var passwords = d.querySelectorAll("[data-password]");


  //
  // Functions
  //

  /**
   * Toggle the visibility of a password field
   * @param {Node} field The password field
   */
  function togglePassword(field) {
    field.type = toggle.checked ? "text" : "password";
  }

  /**
   * Toggle all password fields
   */
  function toggleAllPasswords() {
    passwords.forEach(togglePassword);
  }


  //
  // Init
  //

  // If the toggle is checked, show both passwords (Firefox)
  if (toggle.checked) toggleAllPasswords();

  // Toggle both passwords when the toggle is used
  toggle.addEventListener("change", toggleAllPasswords, false);

})(document);

That concludes part two!

Multiple fields across multiple forms

Once again, I updated my HTML before making any changes to my script. There are now two forms with their own password fields and toggles:

<h2>Change Username</h2>

<p>Enter your username and password to change your username.</p>

<form>
  <div>
    <label for="username">Username</label>
    <input id="username" type="text" name="username">
  </div>

  <div>
    <label for="password">Password</label>
    <input id="password" type="password" name="password" data-password>
  </div>

  <div>
    <label for="show-password">
      <input id="show-password" type="checkbox" data-toggle>
      Show password
    </label>
  </div>

  <div>
    <button type="submit">Change Username</button>
  </div>
</form>

<h2>Change Password</h2>

<p>Enter your current password and new password below.</p>

<form>
  <div>
    <label for="current-password">Current Password</label>
    <input id="current-password" type="password" name="current-password" data-password>
  </div>

  <div>
    <label for="new-password">New Password</label>
    <input id="new-password" type="password" name="new-password" data-password>
  </div>

  <div>
    <label for="show-passwords">
      <input id="show-passwords" type="checkbox" data-toggle>
      Show passwords
    </label>
  </div>

  <div>
    <button type="submit">Change Passwords</button>
  </div>
</form>

Surprise… Once again, I started with an IIFE!

;(function(d) {

  "use strict";

  // Rest of code...

})(document);

I only created one variable this time: toggles. I added a data-toggle attribute to both of the checkboxes in my HTML, so I use it here as my selector string:

// Get all toggles
var toggles = d.querySelectorAll("[data-toggle]");

Next, I created three functions: togglePassword, toggleAllPasswords, and resetToggle. The first two are slightly modified functions from the previous section; the third is a new one I created for the sake of readability.

First, the togglePassword function:

/**
 * Toggle the visibility of a password field
 * @param {Node} field The password field
 */
function togglePassword(field) {
  field.type = event.target.checked ? "text" : "password";
}

The togglePassword function is the same as before, except I’m testing the checked property on the event.target instead: that is, the element which fired the change event. I’m using event delegation; this will make more sense when you see the initialization.

Next, the toggleAllPasswords function:

/**
 * Toggle all password fields inside a form
 * @param {Object} event The Event object
 */
function toggleAllPasswords(event) {
  // Bail if anything other than a toggle fired the event
  if (!event.target.hasAttribute("data-toggle")) return;

  // Get all password fields inside this form
  var passwords = event.target.closest("form").querySelectorAll("[data-password]");

  // Toggle all of the password fields
  passwords.forEach(togglePassword);
}

The toggleAllPasswords function is the most different. First, I check to see which element fired the change event. If it was anything other than one of the toggles, I just quit the function and do nothing else.

Next, I get a reference to all password fields inside the current form. The event.target is the toggle that was activated, so I use the Element.prototype.closest() method to traverse the DOM and find that toggle’s containing form element.

I then use the Element.prototype.querySelectorAll() method to find all password fields inside that form (to all of which I have added the data-password attribute). Finally, I toggle each of these passwords by passing my togglePassword function into the NodeList.prototype.forEach() method.

Last but not least, the resetToggle function:

/**
 * Reset a password toggle back to its default state
 * @param {Node} toggle The password toggle
 */
function resetToggle(toggle) {
  toggle.checked = false;
}

The resetToggle function will allow me to loop through all the toggles and reset them to their default state. This is only necessary because of Firefox’s caching behaviour.

Ulimately, I once again initialize my script:

// Reset all toggles because Firefox caches checkbox state
toggles.forEach(resetToggle);

// Listen for the change event on the body element
// Toggle all passwords inside the appropriate form
d.body.addEventListener("change", toggleAllPasswords, false);

The main difference is that I added my event listener to the body element because I used event delegation. This allowed me to keep my code DRY (Don’t Repeat Yourself) by adding the event listener only once.

Here’s the complete script:

;(function(d) {

  "use strict";

  //
  // Variables
  //

  var toggles = d.querySelectorAll("[data-toggle]");


  //
  // Functions
  //

  /**
   * Toggle the visibility of a password field
   * @param {Node} field The password field
   */
  function togglePassword(field) {
    field.type = event.target.checked ? "text" : "password";
  }

  /**
   * Toggle all password fields inside a form
   * @param {Object} event The Event object
   */
  function toggleAllPasswords(event) {
    // Bail if anything other than a toggle fired the event
    if (!event.target.hasAttribute("data-toggle")) return;

    // Get all password fields inside this form
    var passwords = event.target.closest("form").querySelectorAll("[data-password]");

    // Toggle all of the password fields
    passwords.forEach(togglePassword);
  }

  /**
   * Reset a password toggle back to its default state
   * @param {Node} toggle The password toggle
   */
  function resetToggle(toggle) {
    toggle.checked = false;
  }


  //
  // Init
  //

  // Reset all toggles because Firefox caches checkbox state
  toggles.forEach(resetToggle);

  // Listen for the change event on the body element
  // Toggle all passwords inside the appropriate form
  d.body.addEventListener("change", toggleAllPasswords, false);

})(document);

…And that was the end of this project! Here’s the demo and source code.