Let’s Build: With JavaScript – Dynamic Checkboxes

Welcome to another installment of my Let’s Build: With JavaScript series. This video tutorial teaches you how to make a collection of dynamic HTML checkboxes using vanilla JavaScript.

The ongoing Let’s Build: With JavaScript series is a collection of tutorials that I have compiled to give those new to the language or those looking to finally learn the core components, a fresh take on solving common problems. At my day job, I’m faced with all types of complex problems and scenarios that require JavaScript to solve. Those problems ultimately inspire these tutorials and the future ones to come.

View the codepen

Getting Started

In applications like Dropbox, Asana, Google Drive, and more there is commonly a list of files/tasks/etc. Each list item often allows you to perform certain actions either independently or in bulk. Within those patterns, I often see checkboxes that allow you to narrow in actions on a particular list item. Sometimes you may want to perform bulk actions on several actions. Doing this gets more complex but is very possible.

I’ll be solving a few of those patterns in this tutorial including:

  • Selecting all checkboxes at once
  • Shift + clicking to select multiple checkboxes
  • Clearing all checkbox selections at once
  • Adding an alternate state to those items that are checked

This all happens in less than 90 lines of JavaScript.

HTML

The HTML is basic markup with a few classes used to target the elements via JavaScript and CSS.

<div class="container">
<h1 class="h1">Dynamic Checkboxes with JavaScript</h1>
  <button class="js-clear">Clear <span></span> checked</button>
<table class="table">
  <thead>
    <tr>
      <th align="left"><input type="checkbox" class="js-select-all" />
      </th>
      <th align="left">Publish?</th>
      <th align="left">Author</th>
      <th align="left">Created at</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>
        <div class="published">
          <input type="checkbox" id="1"/>
          <label class="title" for="1">My first blog post</label>
        </div>
      </td>
      <td>Andy</td>
      <td>August 23, 2019</td>
    </tr>
    <tr>
      <td>2</td>
      <td>
        <div class="published">
          <input type="checkbox" id="2"/>
          <label class="title" for="2">My second blog post</label>
        </div>
      </td>
      <td>Randy</td>
      <td>August 22, 2019</td>
    </tr>
    <tr>
      <td>3</td>
      <td>
        <div class="published">
          <input type="checkbox" id="3" />
          <label class="title" for="3"> My third blog post</label>
        </div>
      </td>
      <td>John</td>
      <td>August 21, 2019</td>
    </tr>
    <tr>
      <td>4</td>
      <td>
        <div class="published">
          <input type="checkbox" id="4"/>
          <label class="title" for="4">My fourth blog post</label>
        </div>
      </td>
      <td>Jane</td>
      <td>August 20, 2019</td>
    </tr>
    <tr>
      <td>5</td>
      <td>
        <div class="published">
          <input type="checkbox" id="5"/>
          <label class="title" for="5">My fifth blog post</label>
        </div>
      </td>
      <td>Ryan</td>
      <td>August 19, 2019</td>
    </tr>
    <tr>
      <td>6</td>
      <td>
        <div class="published">
          <input type="checkbox" id="6"/>
          <label class="title" for="6">My sixth blog post</label>
        </div>
      </td>
      <td>Nicole</td>
      <td>August 18, 2019</td>
    </tr>
  </tbody>
</table>
</div><div class="container">
<h1 class="h1">Dynamic Checkboxes with JavaScript</h1>
  <button class="js-clear">Clear <span></span> checked</button>
<table class="table">
  <thead>
    <tr>
      <th align="left"><input type="checkbox" class="js-select-all" />
      </th>
      <th align="left">Publish?</th>
      <th align="left">Author</th>
      <th align="left">Created at</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>
        <div class="published">
          <input type="checkbox" id="1"/>
          <label class="title" for="1">My first blog post</label>
        </div>
      </td>
      <td>Andy</td>
      <td>August 23, 2019</td>
    </tr>
    <tr>
      <td>2</td>
      <td>
        <div class="published">
          <input type="checkbox" id="2"/>
          <label class="title" for="2">My second blog post</label>
        </div>
      </td>
      <td>Randy</td>
      <td>August 22, 2019</td>
    </tr>
    <tr>
      <td>3</td>
      <td>
        <div class="published">
          <input type="checkbox" id="3" />
          <label class="title" for="3"> My third blog post</label>
        </div>
      </td>
      <td>John</td>
      <td>August 21, 2019</td>
    </tr>
    <tr>
      <td>4</td>
      <td>
        <div class="published">
          <input type="checkbox" id="4"/>
          <label class="title" for="4">My fourth blog post</label>
        </div>
      </td>
      <td>Jane</td>
      <td>August 20, 2019</td>
    </tr>
    <tr>
      <td>5</td>
      <td>
        <div class="published">
          <input type="checkbox" id="5"/>
          <label class="title" for="5">My fifth blog post</label>
        </div>
      </td>
      <td>Ryan</td>
      <td>August 19, 2019</td>
    </tr>
    <tr>
      <td>6</td>
      <td>
        <div class="published">
          <input type="checkbox" id="6"/>
          <label class="title" for="6">My sixth blog post</label>
        </div>
      </td>
      <td>Nicole</td>
      <td>August 18, 2019</td>
    </tr>
  </tbody>
</table>
</div>

CSS

The CSS is basic markup to make our tables look more presentable. I also add some custom styling for those items in a checked state.

@import url("https://fonts.googleapis.com/css?family=Montserrat:400,400i,700");

body {
  background: peachpuff;
  font-family: 'Montserrat';
}

.container {
  max-width: 700px;
  margin: 2rem auto;
  background: #fff;
  padding: 40px;
  border-radius: 10px;
}

.table {
  width: 100%;
  margin-bottom: 1rem;
  color: #212529;
  border-collapse: collapse;
}

.table thead th {
  border-bottom: 2px solid #dee2e6;
  border-top: 0;
  vertical-align: bottom;
  user-select: none;
}

.table td, 
.table th {
  padding: 0.75rem;
  border-top: 1px solid #dee2e6;
  user-select: none;
}

td.title {
  font-family: 'Georgia', serif;
  font-style: italic;
}

button {
  background: #f4f4f4;
  padding: 10px 16px;
  margin-bottom: 10px;
  border-radius: 3px;
  appearance: none;
  border: 0;
  border-radius: 8px;
  line-height: normal;

  &:hover {
    background: #f0f0f0;
    cursor: pointer;
  }
}

.published {
  display: flex;
  align-items: center;

  label {
    margin-left: 16px;
    font-family: "Georgia", serif;
    font-size: 16px;
    font-style: italic;
  }
}

input[type=checkbox]:checked + label {
  text-decoration: line-through;
}

Finally, the JavaScript

I use an object-oriented approach towards JavaScript. Everything lives within a global object I created that describes the feature. There are many other ways to write your JavaScript code so I don’t want you to think this is the only way. I found this way quite helpful for me in terms of reusability. I can share logic quite easily between functions and methods within the global object. This also is scoped to the global object which means fewer conflicts with JavaScript written elsewhere.

<br />const DynamicCheckboxes = {
  checkboxes: document.querySelectorAll('.table td input[type="checkbox"]'),
  selectAllTarget: document.querySelector('.js-select-all'),
  clearBtn: document.querySelector('.js-clear'),

  initialize() {
    this.shiftToSelect();
    this.selectAll();
    this.clearChecked();
    this.showRemoveCheckedButton();
  },

  shiftToSelect() {
    const checkboxes = this.checkboxes;
    let lastChecked;

    function handleCheck(event) {
      // Check if shift key is down and check if checkbox is checked
      let inBetween = false;
      if (event.shiftKey && this.checked) {
        checkboxes.forEach(checkbox => {
          if (checkbox === this || checkbox === lastChecked) {
            inBetween = !inBetween;
          }

          if (inBetween) {
            checkbox.checked = true;
          }
        });
      }
      lastChecked = this;
    }


    checkboxes.forEach(checkbox => checkbox.addEventListener('click', handleCheck, false));
  },

  selectAll() {
    function handleSelectAll(event) {
      this.checkboxes.forEach(checkbox => {
        checkbox.checked ? (checkbox.checked = false) : (checkbox.checked = true)
      })
    }

    this.selectAllTarget.addEventListener('click', handleSelectAll.bind(this), false)
  },

  showRemoveCheckedButton() {
    this.clearBtnDisplay('none')
    document.addEventListener('change', this.showBtn.bind(this))
  },

  showBtn(event) {
    const checkboxesChecked = document.querySelectorAll('.table td input[type=checkbox]:checked').length

    if (checkboxesChecked > 0) {
      this.clearBtn.querySelector('span').textContent = checkboxesChecked;
      this.clearBtnDisplay('block');
    } else {
      this.clearBtn.querySelector('span').textContent = '';
      this.clearBtnDisplay('none');
    }
  },

  clearBtnDisplay(state) {
    this.clearBtn.style.display = state;
  },

  clearChecked() {
    this.clearBtn.addEventListener('click', removeChecked.bind(this), false);

    function removeChecked() {
      this.checkboxes.forEach(checkbox => (checkbox.checked = false));
      this.selectAllTarget.checked = false;
      this.clearBtnDisplay('none');
    }
  }

};

DynamicCheckboxes.initialize();

I recommend watching the video to see this come to life and hear my thinking as I’m putting it all together. At first glance, the code looks a bit complicated but in theory, each function within the DynamicCheckboxes object is a building block for the features we wanted to add in the first place. Some functions share logic while others act independently. You can probably guess, scaling something like this is quite hard. That’s likely why the rise of frameworks is all the buzz at the moment.

Closing things out

Hopefully, you’ve learned a bit here! I approached JavaScript in an ignorant manner. I wanted to build everything with it but then realized I needed to take a step back and focus on understanding smaller aspects of problems we encounter day-to-day as developers. There is always going to be better ways to write code but I found that first solving the problem allows you to take a new look at what you’ve accomplished and then later refactor into something more legible and performant. Sometimes writing less code actually can make things harder to read so doing beat yourself up if you look at code that’s been refactored to the most extreme amount.

The Series So Far