3

How do i make a function not fire twice when clicking on text inside a label.

If I use event.preventDefault() then basic browser functionality for making the checkbox checked will stop working too.

const label = document.querySelector('.parent');

label.addEventListener('click', handleLabelClick);

function handleLabelClick(event) {
  console.log('Clicked')
}
<div class="parent">
  <label for="option1">
    <span>Select me</span>
    <input id="option1" type="checkbox">
  </label>
</div>
Penny Liu
  • 15,447
  • 5
  • 79
  • 98

2 Answers2

6

As I understand it, you want clicks on .parent elements to fire a click handler, but you don't want that handler fired for clicks related to a checkbox or its label within .parent.

Two ways to do that:

  1. Add a handler for the label that calls stopPropagation, or

  2. Check within the event handler whether the event passed through the label

Here's approach #1:

const parent = document.querySelector('.parent');

parent.addEventListener('click', handleLabelClick);

// Stop clicks in the label or checkbox from propagating to parent
parent.querySelector("label").addEventListener("click", function(event) {
  event.stopPropagation();
});

function handleLabelClick(event) {
 console.log('Clicked');
}
.parent {
  border: 1px solid #ddd;
}
<div class="parent">
  <label for="option1">
    <span>Select me</span>
    <input id="option1" type="checkbox">
  </label>
</div>

Here's approach #2:

const parent = document.querySelector('.parent');

parent.addEventListener('click', handleLabelClick);

function handleLabelClick(event) {
  const label = event.target.closest("label");
  if (label && this.contains(label)) {
    // Ignore this click
    return;
  }
 console.log('Clicked');
}
.parent {
  border: 1px solid #ddd;
}
<div class="parent">
  <label for="option1">
    <span>Select me</span>
    <input id="option1" type="checkbox">
  </label>
</div>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • @KlaudioMilankovic - I'm afraid I don't understand what you're describing. – T.J. Crowder Oct 18 '18 at 08:13
  • 1
    So, lets say is a component on it's own and you place this inside other component named 'parent'. Once you attach event listener to a parent and click on a label it fires twice instead of once. I am trying NOT to handle clicks on label as this is done by the browser. – Klaudio Milankovic Oct 18 '18 at 08:17
  • @KlaudioMilankovic - I think I understand. So in your example, you want the `handleLabelClick` to fire for clicks unrelated to the checkbox, and not for clicks related to the checkbox? – T.J. Crowder Oct 18 '18 at 08:18
  • 1
    Exactly. I want to retain default browser functionality for checkboxes but not have it fire my function twice which is attached to a parent. – Klaudio Milankovic Oct 18 '18 at 08:20
  • @KlaudioMilankovic - Good deal. I've updated the answer. – T.J. Crowder Oct 18 '18 at 08:24
  • Option 1 was the solution for my issue. Thank you so much! – LReeder14 Jan 24 '19 at 20:41
2

This is a standard (unfortunate) browser behavior.

Attribute for assigns <label> to <input>, so when <label> element is clicked, browser will emulate a click on <input> element right after your real click.

On a plus side, this allows focus of <input type="text", switching of <input type="radio", or toggling of <input type="checkbox".
But for the unfortunate side, this also causes that both elements send the click event. In case you are listening on clicks on a parent element, this means that you'll receive one "human interaction" twice. Once from <input> and once from "the clicked element".

For those who wonder, <input> could be inside <label> element, you'll get less styling possibility, but you can then click in between check-box and text.

Your putting <span> and <input> inside <label> actually creates a nice test case.
Try clicking from left to right;
On the text, you'll receive SPAN + INPUT events,
between text and check-box you'll get LABEL + INPUT events,
on the check-box directly only INPUT event,
then further right, only DIV event. (because DIV is a block element and spans all the way to the right)

One solution would be to listen only on <input> element events, but then you will not capture <div> clicks and you also can't put <div> inside <label>.

The simplest thing to do is to ignore clicks on <label> and all clickable elements inside <label> except <input>. In this case <span>.

const elWrapper = document.querySelector('.wrapper');

elWrapper.addEventListener('click', handleLabelClick);

function handleLabelClick(event) {
 console.log('Event received from tagName: '+event.target.tagName);
 if (event.target.tagName === 'LABEL' || event.target.tagName === 'SPAN') { return; }
 console.log('Performing some action only once. Triggered by click on: '+event.target.tagName);
}
<div class="wrapper">
  <label for="option1">
    <span>Select me</span>
    <input id="option1" type="checkbox">
  </label>
</div>

(from OP's example I changed parent to wrapper and label to elWrapper, because parent and label are keywords.)

The solution from @T.J. causes events from <label> and everything inside it to be ignored down-the-road. To get the event fired, you'd need to click somewhere on the <div>, but not directly on the text or check-box.

I added my answer because I didn't think this was the OP's intention.

But even for this other case, you might use similar approach as I offered above. Checking if clicked element name is DIV then allowing further actions. I think it's more straightforward, more localized (doesn't affect event propagation), more universal (if you select capturing: addEventListener(...,...,true) this will still work)

papo
  • 1,606
  • 19
  • 18
  • Thank you @papo! I just asked [a similar question](https://stackoverflow.com/q/59252625/1795402) that actually started as observing this behavior. Your explanation of the 'why' clarifies things for me. – Daniel Brady Dec 09 '19 at 16:40