Search code examples
javascriptevent-handlingdom-eventsevent-delegation

How does one handle DOM events from various buttons by a single handler function


I'm not sure if it's possible, but I'd like to use one unique function to trigger 4 different buttons to count a value (+ and -). But there are four different span values, for example, if I trigger forest it will only add or remove from forest, if I do it for town it will only trigger for town, and so on.

// set inital value to zero
let count = 0;
// select value and buttons
const valueForest = document.querySelector("#valueForest");
const btns = document.querySelectorAll(".btn");

btns.forEach(function (btn) {
  btn.addEventListener("click", function (e) {
    const styles = e.currentTarget.classList;
    if (styles.contains("decrease")) {
      count--;
    } else if (styles.contains("increase")) {
      count++;
    } else {
      count = 0;
    }

    if (count > 0) {
      valueForest.style.color = "green";
    }
    if (count < 0) {
      valueForest.style.color = "red";
    }
    if (count === 0) {
      valueForest.style.color = "#222";
    }
    valueForest.textContent = count;
  });
 });
<div class="scoreDiv">
  <h3>Input below the quantity of each tile in the end of the game:</h3>
  <div class="scoreItem">
      <h4>Forest</h4>
      <button class="btn decrease">-</button>
      <span class="value" id="valueForest">0</span>
      <button class="btn increase">+</button>
      <h4>SOMA</h4>
  </div>
  <div class="scoreItem">
      <h4>Town</h4>
      <button class="btn decrease">-</button>
      <span class="value" id="valueTown">0</span>
      <button class="btn increase">+</button>
      <h4>SOMA</h4>
  </div>
  <div class="scoreItem">
      <h4>Production</h4>
      <button class="btn decrease">-</button>
      <span class="value" id="valueProduction">0</span>
      <button class="btn increase">+</button>
      <h4>SOMA</h4>
  </div>
  <div class="scoreItem">
      <h4>Factory</h4>
      <button class="btn decrease">-</button>
      <span class="value" id="valueFactory">0</span>
      <button class="btn increase">+</button>
      <h4>SOMA</h4>
  </div>
</div>


Solution

  • yes, with event delegation

    this way:

    const scoreDiv = document.querySelector('div.scoreDiv') // the parent Div
    
    scoreDiv.onclick = e => // get all clicks everywhere upon this parent Div
      {
      if (!e.target.matches('div.scoreItem > button.btn ')) return  // ignore other clicks
    
      let countEl =  e.target.closest('div.scoreItem').querySelector('span.value')
        , newVal  = +countEl.textContent + (e.target.matches('.decrease') ? -1 : +1)
        ;
      countEl.style.color = (newVal > 0) ? 'green' :  (newVal < 0) ? 'red' : '#222'
      countEl.textContent = newVal;
      }
    span.value {
      display       : inline-block; 
      width         : 5em; 
      text-align    : right; 
      padding-right : .5em;
      font-weight   : bold;
    }
    <div class="scoreDiv">
      <h3>Input below the quantity of each tile in the end of the game:</h3>
      <div class="scoreItem">
        <h4>Forest</h4>
        <button class="btn decrease">-</button>
        <span class="value" id="valueForest">0</span>
        <button class="btn increase">+</button>
        <h4>SOMA</h4>
    </div>
      <div class="scoreItem">
        <h4>Town</h4>
        <button class="btn decrease">-</button>
        <span class="value" id="valueTown">0</span>
        <button class="btn increase">+</button>
        <h4>SOMA</h4>
      </div>
      <div class="scoreItem">
        <h4>Production</h4>
        <button class="btn decrease">-</button>
        <span class="value" id="valueProduction">0</span>
        <button class="btn increase">+</button>
        <h4>SOMA</h4>
      </div>
      <div class="scoreItem">
        <h4>Factory</h4>
        <button class="btn decrease">-</button>
        <span class="value" id="valueFactory">0</span>
        <button class="btn increase">+</button>
        <h4>SOMA</h4>
      </div>
    </div>

    Explanations about

    if (!e.target.matches('div.scoreItem > button.btn')) return  
    

    First of all the event handler scoreDiv.onclick = e => concern everything inside

    <div class="scoreDiv"> 
      // everything inside
    </div>
    

    So this get any click event in this space is processed by this arrow function.
    It could be a click:
    on the H3 element
    , or one of the span elements
    , or any the H4 elements
    , everything !
    , even the spaces between any elements.

    the event [e] have diffrents properties
    e.currentTarget --> is a reference to the caller element (here it is scoreDiv [div.scoreItem]) e.target --> is a reference to the element where the click happen

    for this job we need to do only increment / decrement operations.
    that's mean we have to ignore any click event witch is not on the plus or minus buttons.
    This 8 buttons are : <button class="btn decrease">-</button> or <button class="btn increase">-</button>

    All this buttons correspond to CSS = div.scoreItem > button.btn

    In javascript the code for testing that is

    e.target.matches('div.scoreItem > button.btn')
    

    will return a boolean value (true or false)

    There is now a strategy: Instead of making a big

    if ( e.target.matches('div.scoreItem > button.btn') ) 
      { 
      //...
      // with many lines of code 
      //until the closing 
      }
    // and then quit the function
    

    and because this is a function we use a Logical NOT (!)
    to make a direct return from function, coded like that:

    if (!e.target.matches('div.scoreItem > button.btn')) return  
    

    The main interest is to quickly free the event manager in case of another element (present in scoreDiv) have is own click eventHandler.