1

Goal

Avoid unnecessary event bindings.

Sample code

Comment box with a reply button for each individual comment

const btns = document.getElementsByClassName('reply-btn');

for (let i = 0; i < btns.length; i++) {    
  btns[i].addEventListener('click', showCommentContentAsPreview);
}


function showCommentContentAsPreview(e) {
  console.log('showCommentContentAsPreview()');
  
  // CHECK IF THIS BUTTON ALREADY BINDED !!!
  const previewDiv = document.getElementById('preview');
  const commentId = e.target.getAttribute('data-comment-id')
  const commentDiv = document.getElementById('comment-' + commentId);
  const commentText = commentDiv.querySelector('p').innerText
  const closeReplyBtn = previewDiv.querySelector('button');
  const previewContent  = previewDiv.querySelector('.preview-content');
  
  // set to preview
  previewContent.innerText = commentText;
  
  // show reply close button  
  closeReplyBtn.classList.remove('hidden');
  
  // bind EventListener to "reply close button"
  closeReplyBtn.addEventListener('click', closeReply)
  
  function closeReply() {
    console.log('bind to btn');
    previewContent.innerText = '';
    this.removeEventListener('click', closeReply);
    closeReplyBtn.classList.add('hidden');
  }
}
.hidden {
  display: none;
}

.comment {
  border-bottom: 1px solid #000;
  padding: 5px;
}


.preview {
  background-color: #ccc;
  padding: 20px;
  margin-top: 20px;
}
<div>
  <!-- comment list -->
  <div id="comment-1" class="comment">
    <p>Comment Content 1</p>
    <button class="reply-btn" data-comment-id="1">reply</button>
  </div>
  
  <div id="comment-2"  class="comment">
    <p>Comment Content 2</p>
    <button  class="reply-btn" data-comment-id="2">reply</button>
  </div>  
</div>

<!-- output -->
<div>
  <div id="preview" class="preview">
    <div class="preview-content"></div>
    <button class="hidden">Close Preview</button>
  </div>
</div>

Simulate problem

When you try the example, the following two scenarios occur:

  1. Click reply once and then click "close preview"

  2. Click on reply several times and then on "close preview".

Question

How can I avoid multiple bindings to the same button? I am already thinking about singleton.

Maik Lowrey
  • 15,957
  • 6
  • 40
  • 79
  • I think this has an answer here - https://stackoverflow.com/questions/28056716/check-if-an-element-has-event-listener-on-it-no-jquery/47337711. One of the answers suggests that it doesn't matter if multiple listeners are added to it. – Zephyr Nov 29 '21 at 11:58
  • @Zephyr So you think it doesn't matter if multiple identical EventListeners are attached to the same object? – Maik Lowrey Nov 29 '21 at 12:08
  • 1
    Yes, for equivalent function references it should be fine as mentioned [here](https://stackoverflow.com/a/47337711/11561643). This [link](https://stackoverflow.com/a/28056815/11561643) also suggests adding a boolean to keep track of the listener – Zephyr Nov 29 '21 at 12:21

2 Answers2

1

Instead of binding a listener to every element in the series, you can bind a single listener once on a common parent of them all, and then use element.matches() to determine if the click target is the one that you want before doing more work. See the following example:

function logTextContent (elm) {
  console.log(elm.textContent);
}

function handleClick (ev) {
  if (ev.target.matches('.item')) {
    logTextContent(ev.target);
  }
}

document.querySelector('ul.list').addEventListener('click', handleClick);
<ul class="list">
  <li class="item">Item 1</li>
  <li class="item">Item 2</li>
  <li class="item">Item 3</li>
  <li class="item">Item 4</li>
  <li class="item">Item 5</li>
</ul>
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
0

With the helpful hints from @Zephyr and @jsejcksn I have rewritten the code of the above question. Thus I have achieved my goal of avoiding multiple identical bindings to one element.

const container = document.getElementById('comment-container');
const previewDiv = document.getElementById('preview');
const closeReplyBtn = previewDiv.querySelector('button');
const previewContent  = previewDiv.querySelector('.preview-content');

container.addEventListener('click', handleClick);

function handleClick(ev) {   
  
    if (ev.target.matches('.reply-btn')) {      
      if (ev.target.getAttribute('listener') !== 'true') { 
         removeOtherListenerFlags();
         ev.target.setAttribute('listener', 'true');
         showCommentContentAsPreview(ev);
      }      
    }
    if (ev.target.matches('#preview button')) {        
      previewContent.innerText = '';     
      closeReplyBtn.classList.add('hidden');      
      removeOtherListenerFlags();
    }
}

function showCommentContentAsPreview(e) {
  console.log('showCommentContentAsPreview()');  
  const commentId = e.target.getAttribute('data-comment-id')
  const commentDiv = document.getElementById('comment-' + commentId);
  const commentText = commentDiv.querySelector('p').innerText
  
  // set to preview
  previewContent.innerText = commentText;
  
  // show reply close button  
  closeReplyBtn.classList.remove('hidden');
}

function removeOtherListenerFlags() {
   const replyBtns = container.querySelectorAll('.reply-btn')
   Object.keys(replyBtns).forEach((el) => {
     replyBtns[el].removeAttribute('listener');     
   })   
}
.hidden {
  display: none;
}

.comment {
  border-bottom: 1px solid #000;
  padding: 5px;
}


.preview {
  background-color: #ccc;
  padding: 20px;
  margin-top: 20px;
}
<div id="comment-container">
  <div id="comment-listing">
    <!-- comment list -->
    <div id="comment-1" class="comment">
      <p>Comment Content 1</p>
      <button class="reply-btn" data-comment-id="1">reply 1</button>
    </div>

    <div id="comment-2"  class="comment">
      <p>Comment Content 2</p>
      <button  class="reply-btn" data-comment-id="2">reply 2</button>
    </div>  
  </div>

  <!-- output -->
  <div>
    <div id="preview" class="preview">
      <div class="preview-content"></div>
      <button class="hidden">Close Preview</button>
    </div>
  </div>
  
</div>

Cool and Thanks!

Maik Lowrey
  • 15,957
  • 6
  • 40
  • 79