Home > front end >  Event delegation does not work if the bound target is nested
Event delegation does not work if the bound target is nested

Time:01-20

For a comment list I use the event delegation pattern after a recommendation from Stackoverflow colleagues (mplungjan, Michel). It works well and I am very enthusiastic about this pattern. But as I already suspected, there will be problems if the bound element (button) contains two child elements (span, span).

Since I want to get the CommentID from the target in the parent element of the child element, it only works in the cases when you click exactly between the spans inside the button. Actually a case for currentTarget but that doesn't work in this case because the tapped element is the whole comment list.

Question: What do I have to do to fix it?

const commentList = document.querySelector('.comment-list');

commentList.addEventListener('click', (ev) => {
  console.log('1. clicked');
  const getObjectId = () => {
    return ev.target.parentNode.parentNode.getAttribute('data-comment-id');
  }
  
  if (! getObjectId()) return false;

  if (ev.target.classList.contains('delete')) {
    console.log('2. Delete action');
    console.log('3. for relatedID', getObjectId());
  }
  
  if (ev.target.classList.contains('edit')) {
    console.log('2. Edit action');
    console.log('3. for relatedID', getObjectId());
  }  
  
  if (ev.target.classList.contains('flag')) {
    console.log('2. Flag action');
    console.log('3. for relatedID', getObjectId());
  }    
  
});
.controller {
  display: flex;
  gap:20px;
}
.comment {
  margin-bottom: 20px;
  background: gray;
}

.controller button > span {
  background: orange;
}

.controller button span:first-child {
  margin-right: 10px;
}
<div >
  <div >
    <div >lorem 1. Dont work! Nested button.</div>
    <div  data-comment-id="1">
      <div >
        <button ><span>delete</span><span>ICON</span></button>        
      </div>
      <div >
        <button ><span>edit</span><span>ICON</span></button>
      </div>
      <div >
        <button ><span>flag</span><span>ICON</span></button>          
      </div>
    </div>
  </div>
  
  <div >
    <div >lorem 2. Work! </div>
    <div  data-comment-id="2">
      <div ><button >delete</button></div>
      <div ><button >edit</button></div>
      <div ><button >flag</button></div>
    </div>
  </div>
  
  <div >
    <div >lorem 3. Work! </div>
    <div  data-comment-id="3">
      <div ><button >delete</button></div>
      <div ><button >edit</button></div>
      <div ><button >flag</button></div>
    </div>
  </div>  
  
</div>

CodePudding user response:

The problem is that you're using .parentNode.parentNode to get to the element with data-comment-id, but the number of parents changes when the target is nested inside additional <span> elements.

Don't hard-code the nesting levels, use .closest() to find the containing controller node.

  const getObjectId = () => {
    return ev.target.closest('.controller').getAttribute('data-comment-id');
  }

CodePudding user response:

Building on my last comment in the other question

const tgtButtonWhenSpansInsideButton = e.target.closest("button")

  1. Cache the objects
  2. the closest method will get the button itself even if no children
  3. Make sure you get the class from the containing element of what you want to call a button

const commentList = document.querySelector('.comment-list');
const getObjectId = (tgt) => tgt.closest('.controller').dataset.commentId;

commentList.addEventListener('click', (ev) => {
  const tgt = ev.target.closest("button")
  const objectId = getObjectId(tgt);
  if (!objectId) return;
  console.log(objectId,"clicked")
  if (tgt.classList.contains('delete')) {
    console.log('2. Delete action');
    console.log('3. for relatedID', objectId);
  }

  if (tgt.classList.contains('edit')) {
    console.log('2. Edit action');
    console.log('3. for relatedID', objectId);
  }

  if (tgt.classList.contains('flag')) {
    console.log('2. Flag action');
    console.log('3. for relatedID', objectId);
  }

});
.controller {
  display: flex;
  gap: 20px;
}

.comment {
  margin-bottom: 20px;
  background: gray;
}

.controller button>span {
  background: orange;
}

.controller button span:first-child {
  margin-right: 10px;
}
<div >
  <div >
    <div >lorem 1. Dont work! Nested button.</div>
    <div  data-comment-id="1">
      <div >
        <button ><span>delete</span><span>ICON</span></button>
      </div>
      <div >
        <button ><span>edit</span><span>ICON</span></button>
      </div>
      <div >
        <button ><span>flag</span><span>ICON</span></button>
      </div>
    </div>
  </div>

  <div >
    <div >lorem 2. Work! </div>
    <div  data-comment-id="2">
      <div ><button >delete</button></div>
      <div ><button >edit</button></div>
      <div ><button >flag</button></div>
    </div>
  </div>

  <div >
    <div >lorem 3. Work! </div>
    <div  data-comment-id="3">
      <div ><button >delete</button></div>
      <div ><button >edit</button></div>
      <div ><button >flag</button></div>
    </div>
  </div>

</div>

CodePudding user response:

In this case I would "traverse" the DOM up if it wasn't a button that was clicked, something like this

const commentList = document.querySelector('.comment-list');

commentList.addEventListener('click', (ev) => {
  console.log('1. clicked', ev.target.tagName);

  let target = ev.target
  if (target.tagName === "SPAN") {
    target = target.parentElement
  }

  const commentId = target.parentElement.parentElement.getAttribute('data-comment-id');
  
  if (!commentId) return false;

  if (target.classList.contains('delete')) {
    console.log('2. Delete action');
    console.log('3. for relatedID', commentId);
  }
  
  if (target.classList.contains('edit')) {
    console.log('2. Edit action');
    console.log('3. for relatedID', commentId);
  }  
  
  if (target.classList.contains('flag')) {
    console.log('2. Flag action');
    console.log('3. for relatedID', commentId);
  }    
  
});
.controller {
  display: flex;
  gap:20px;
}
.comment {
  margin-bottom: 20px;
  background: gray;
}

.controller button > span {
  background: orange;
}

.controller button span:first-child {
  margin-right: 10px;
}
<div >
  <div >
    <div >lorem 1. Dont work! Nested button.</div>
    <div  data-comment-id="1">
      <div >
        <button ><span>delete</span><span>ICON</span></button>        
      </div>
      <div >
        <button ><span>edit</span><span>ICON</span></button>
      </div>
      <div >
        <button ><span>flag</span><span>ICON</span></button>          
      </div>
    </div>
  </div>
  
  <div >
    <div >lorem 2. Work! </div>
    <div  data-comment-id="2">
      <div ><button >delete</button></div>
      <div ><button >edit</button></div>
      <div ><button >flag</button></div>
    </div>
  </div>
  
  <div >
    <div >lorem 3. Work! </div>
    <div  data-comment-id="3">
      <div ><button >delete</button></div>
      <div ><button >edit</button></div>
      <div ><button >flag</button></div>
    </div>
  </div>  
  
</div>

  • Related