Pete
Pete

Reputation: 58462

Mutation observer fires twice

I have the following code that works, but if I click a panel and then click it again, the mutation observer fires twice on the second click. Does anyone know the cause of this and how to stop it?

I'm guessing it's because it fires once for the add class and once for the remove class. but if that's the case, the classList.contains shouldn't be true when you do the remove class (as can be seen if you click on a different panel - only that one add class event fires)

const activeClass = 'active';
const $panels = $('.panel');
let counter = 0;

$panels.on('click', e => {
  const $panel = $(e.currentTarget);
  $panels.removeClass(activeClass);
  $panel.addClass(activeClass);
});

$panels.each((index, panel) => {
  const observer = new MutationObserver(mutationsList => {
    for (let i = 0; i < mutationsList.length; i++) {
      if (mutationsList[i].attributeName === 'class' && mutationsList[i].target.classList.contains(activeClass)) {
        console.log(mutationsList[i].target.className, counter);
        counter++;
      }
    }
  });

  observer.observe(panel, {
    attributes: true,
    childList: false,
    subtree: false
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="panel panel--1">
panel
</div>
<div class="panel panel--2">
panel
</div>
<div class="panel panel--3">
panel
</div>

Upvotes: 3

Views: 3780

Answers (3)

Pete
Pete

Reputation: 58462

After a lot of debugging, I have found that for each action on the class list, a new mutation record is added to the mutation list. However, the target of the mutation record is the final state of the element, and not the state of the element when the event occurred.

To get around this, I have had do a test on the target's class name to see if it has already had a mutation fired (luckily my elements have unique classes) and if not fire the function.

This probably isn't the best was of doing things but seems to solve my problem so if anyone has a better way then please feel free to post it

const activeClass = 'active';
const $panels = $('.panel');

$panels.on('click', e => {
  const $panel = $(e.currentTarget);
  $panel.removeClass(activeClass);
  $panel.addClass(activeClass);
});

$panels.each((index, panel) => {
  const observer = new MutationObserver(mutationsList => {
    const groupedMutations = [];
    for (let i = 0; i < mutationsList.length; i++) {
      if (mutationsList[i].attributeName === 'class' &&
        groupedMutations.indexOf(mutationsList[i].target.className) == -1 &&
        mutationsList[i].target.classList.contains(activeClass)) {
        groupedMutations.push(mutationsList[i].target.className);
        console.log(mutationsList[i].target.className, i)
      }
    }
  });

  observer.observe(panel, {
    attributes: true,
    childList: false,
    subtree: false
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="panel panel--1">
  panel
</div>
<div class="panel panel--2">
  panel
</div>
<div class="panel panel--3">
  panel
</div>

Upvotes: 1

Caleb Hillary
Caleb Hillary

Reputation: 753

Mutation Observer is an asynchronous. This means that it keeps a record of all the changes, and calls the callback function once passing the full record of changes done to the DOM.

After clicking once when you click again

Record

  1. class removed
  2. class added

Current status: Class added (when callback is called)

So, classlist contains returns true.

Upvotes: 0

woxxom
woxxom

Reputation: 73846

Your code makes two mutations when there's an active panel regardless of which panel we click:

  • Record 1: removing the class
  • Record 2: adding the class

The difference in behavior you observe for the same-element click is caused by your handler's if condition. It may be surprising but it sees the active class is present on the element in the first mutation record (for removing) because MutationObserver's handler runs as a micro-task - meaning it runs after the current task completes - meaning that addClass() has already added the class on the same element.

As a solution, you can skip the work if the clicked element is already active:

$panels.on('click', e => {
  const $panel = $(e.currentTarget);
  if (!$panel.hasClass(activeClass)) {
    $panels.removeClass(activeClass);
    $panel.addClass(activeClass);
  }
});

Upvotes: 1

Related Questions