Dustin Venable
Dustin Venable

Reputation: 49

JQuery - Event Delegation - What is happening in my code?

I've read as much as I can on event delegation, and I cannot figure out why my code is acting the way it is. When the page loads, all of the check buttons work properly (toggles between 2 classes). Whenever I add a new item to the shopping list, the check button works for ONLY that new item. If I add a 2nd new item, the check button works for that new item, the 4 original items, but NOT the first new item. If I add any more items, it will not work for the previous item, but will work for all items before that one.

How can I get the check button to work for all items?

HTML...

<body>

  <div class="container">
    <h1>Shopping List</h1>

    <form id="js-shopping-list-form">
      <label for="shopping-list-entry">Add an item</label>
      <input type="text" name="shopping-list-entry" id="shopping-list-entry" placeholder="e.g., broccoli">
      <button type="submit">Add item</button>
    </form>

    <ul class="shopping-list">
      <li>
        <span class="shopping-item">apples</span>
        <div class="shopping-item-controls">
          <button class="shopping-item-toggle">
            <span class="button-label">check</span>
          </button>
          <button class="shopping-item-delete">
            <span class="button-label">delete</span>
          </button>
        </div>
      </li>
      <li>
        <span class="shopping-item">oranges</span>
        <div class="shopping-item-controls">
          <button class="shopping-item-toggle">
            <span class="button-label">check</span>
          </button>
          <button class="shopping-item-delete">
            <span class="button-label">delete</span>
          </button>
        </div>
      </li>
      <li>
        <span class="shopping-item shopping-item__checked">milk</span>
        <div class="shopping-item-controls">
          <button class="shopping-item-toggle">
            <span class="button-label">check</span>
          </button>
          <button class="shopping-item-delete">
            <span class="button-label">delete</span>
          </button>
        </div>
      </li>
      <li>
        <span class="shopping-item">bread</span>
        <div class="shopping-item-controls">
          <button class="shopping-item-toggle">
            <span class="button-label">check</span>
          </button>
          <button class="shopping-item-delete">
            <span class="button-label">delete</span>
          </button>
        </div>
      </li>
    </ul>
  </div>
<script
  src="https://code.jquery.com/jquery-3.4.1.js"
  integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
  crossorigin="anonymous"></script>
<script src="script.js"></script>
</body>
</html>

CSS...

* {
    box-sizing: border-box;
  }

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

  button, input[type="text"] {
    padding: 5px;
  }

  button:hover {
    cursor: pointer;
  }

  #shopping-list-item {
    width: 250px;
  }

  .container {
    max-width: 600px;
    margin: 0 auto;
  }

  .shopping-list {
    list-style: none;
    padding-left: 0;
  }

  .shopping-list > li {
    margin-bottom: 20px;
    border: 1px solid grey;
    padding: 20px;
  }

  .shopping-item {
    display: block;
    color: grey;
    font-style: italic;
    font-size: 20px;
    margin-bottom: 15px;
  }

  .shopping-item__checked {
    text-decoration: line-through;
  }

JS...

//Make the Check Button functional
function checkButton() {
    $(".shopping-item-toggle").on("click", "span", function() {
    $(this).closest("li").find(".shopping-item").toggleClass("shopping-item__checked");
  })};
checkButton();

//Make the Delete Button functional
function delButton() {
    $(".shopping-item-delete").on("click", function() {
    $(this).parent().parent().remove();
  })};
delButton();

  //Create a variable called buttons to add the 2 buttons in function below
  let buttons = "<div class='shopping-item-controls'><button class='shopping-item-toggle'>\
                 <span class='button-label'>check</span></button>\
                 <button class='shopping-item-delete'><span class='button-label'>delete</span></button></div>";

  //Add new Item to List
  $("#js-shopping-list-form").submit(function() {
    let item = $("#shopping-list-entry").val();
    if (item != "") { //As long as the input has text in it, run the function below
    $(".shopping-list").append("<li>" + "<span class='shopping-item'>" + item + "</span>" + buttons + "</li>");
    event.preventDefault();
    checkButton();//Make the check button functional
    delButton();//Make the delete button functional
    } else { //if NO text is in the input, show this alert
      alert("Must enter an item name!");
    }
  });

Upvotes: 2

Views: 47

Answers (1)

showdev
showdev

Reputation: 29168

Whenever you add a new item, new click handlers are bound to all items. These means that items that existed on the page might get two or more duplicate handlers. If two handlers are bound to an item, the first one will toggleClass to turn on the "checked" class and the second one will immediately toggle the class back off.

To use event delegation, I recommend binding the handlers only once, and binding them to an ancestor that always exists on the page.

For example:

// Bind "check" and "delete" handlers to the ".shopping-list" ancestor.

$(".shopping-list").on("click", ".shopping-item-toggle span", function() {
  $(this).closest("li").find(".shopping-item").toggleClass("shopping-item__checked");
});

$(".shopping-list").on("click", '.shopping-item-delete', function() {
  $(this).parent().parent().remove();
});


// Create a variable called buttons to add the 2 buttons in function below
let buttons = "<div class='shopping-item-controls'><button class='shopping-item-toggle'>\
                   <span class='button-label'>check</span></button>\
                   <button class='shopping-item-delete'><span class='button-label'>delete</span></button>\
               </div>";

//Add new Item to List
$("#js-shopping-list-form").submit(function(event) {
  event.preventDefault();
  let item = $("#shopping-list-entry").val();
  if (item != "") { // If the input has text in it, add the new item
    $(".shopping-list").append("<li>" + "<span class='shopping-item'>" + item + "</span>" + buttons + "</li>");
  } else { //if NO text is in the input, show this alert
    alert("Must enter an item name!");
  }
});
* {
  box-sizing: border-box;
}

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

button,
input[type="text"] {
  padding: 5px;
}

button:hover {
  cursor: pointer;
}

#shopping-list-item {
  width: 250px;
}

.container {
  max-width: 600px;
  margin: 0 auto;
}

.shopping-list {
  list-style: none;
  padding-left: 0;
}

.shopping-list>li {
  margin-bottom: 20px;
  border: 1px solid grey;
  padding: 20px;
}

.shopping-item {
  display: block;
  color: grey;
  font-style: italic;
  font-size: 20px;
  margin-bottom: 15px;
}

.shopping-item__checked {
  text-decoration: line-through;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="container">
  <h1>Shopping List</h1>

  <form id="js-shopping-list-form">
    <label for="shopping-list-entry">Add an item</label>
    <input type="text" name="shopping-list-entry" id="shopping-list-entry" placeholder="e.g., broccoli">
    <button type="submit">Add item</button>
  </form>

  <ul class="shopping-list">
    <li>
      <span class="shopping-item">apples</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
    <li>
      <span class="shopping-item">oranges</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
    <li>
      <span class="shopping-item shopping-item__checked">milk</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
    <li>
      <span class="shopping-item">bread</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
  </ul>
</div>


Here's a demonstration of how multiple handlers are fired after adding a new item:

//Make the Check Button functional
function checkButton() {
  $(".shopping-item-toggle").on("click", "span", function() {
    let $item = $(this).closest("li").find(".shopping-item");
    console.log('Check Handler ' + ($item.hasClass('shopping-item__checked') ? 'OFF' : 'ON'));
    $item.toggleClass("shopping-item__checked");
  })
};
checkButton();

//Make the Delete Button functional
function delButton() {
  $(".shopping-item-delete").on("click", function() {
    console.log('Delete Handler');
    $(this).parent().parent().remove();
  })
};
delButton();

//Create a variable called buttons to add the 2 buttons in function below
let buttons = "<div class='shopping-item-controls'><button class='shopping-item-toggle'>\
                 <span class='button-label'>check</span></button>\
                 <button class='shopping-item-delete'><span class='button-label'>delete</span></button></div>";

//Add new Item to List
$("#js-shopping-list-form").submit(function(e) {
  event.preventDefault();
  let item = $("#shopping-list-entry").val();
  if (item != "") { //As long as the input has text in it, run the function below
    $(".shopping-list").append("<li>" + "<span class='shopping-item'>" + item + "</span>" + buttons + "</li>");
    checkButton(); //Make the check button functional
    delButton(); //Make the delete button functional
  } else { //if NO text is in the input, show this alert
    alert("Must enter an item name!");
  }
});
* {
  box-sizing: border-box;
}

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

button,
input[type="text"] {
  padding: 5px;
}

button:hover {
  cursor: pointer;
}

#shopping-list-item {
  width: 250px;
}

.container {
  max-width: 600px;
  margin: 0 auto;
}

.shopping-list {
  list-style: none;
  padding-left: 0;
}

.shopping-list>li {
  margin-bottom: 20px;
  border: 1px solid grey;
  padding: 20px;
}

.shopping-item {
  display: block;
  color: grey;
  font-style: italic;
  font-size: 20px;
  margin-bottom: 15px;
}

.shopping-item__checked {
  text-decoration: line-through;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="container">
  <h1>Shopping List</h1>

  <form id="js-shopping-list-form">
    <label for="shopping-list-entry">Add an item</label>
    <input type="text" name="shopping-list-entry" id="shopping-list-entry" placeholder="e.g., broccoli">
    <button type="submit">Add item</button>
  </form>

  <ul class="shopping-list">
    <li>
      <span class="shopping-item">apples</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
    <li>
      <span class="shopping-item">oranges</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
    <li>
      <span class="shopping-item shopping-item__checked">milk</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
    <li>
      <span class="shopping-item">bread</span>
      <div class="shopping-item-controls">
        <button class="shopping-item-toggle"><span class="button-label">check</span></button>
        <button class="shopping-item-delete"><span class="button-label">delete</span></button>
      </div>
    </li>
  </ul>
</div>

Upvotes: 2

Related Questions