Reputation: 58462
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
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
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
Current status: Class added
(when callback is called)
So, classlist contains returns true.
Upvotes: 0
Reputation: 73846
Your code makes two mutations when there's an active panel regardless of which panel we click:
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