45

I'd like my event to be triggered when a div tag containing a trigger class is changed.

I have no idea how to make it listen to the class' adding event.

<div id="test">test</div>

<script type="text/javascript">
    document.getElementById.setAttribute("class", "trigger");

    function workOnClassAdd() {
       alert("I'm triggered");
    }
</script>
Jignesh Joisar
  • 11,406
  • 3
  • 52
  • 51
Patrick Jeon
  • 1,524
  • 5
  • 23
  • 31
  • 1
    Do you for sure know the class will be updated with `.setAttribute()` or are you talking in more arbitrary terms (i.e. you'd like the event handler to fire even if a user changes the class through Firebug)? – jaredhoyt May 16 '12 at 04:42
  • 1
    FYI http://stackoverflow.com/questions/1950038/jquery-fire-event-if-css-class-changed – Dasarp May 16 '12 at 04:50

5 Answers5

69

The future is here, and you can use the MutationObserver interface to watch for a specific class change.

let targetNode = document.getElementById('test')

function workOnClassAdd() {
    alert("I'm triggered when the class is added")
}

function workOnClassRemoval() {
    alert("I'm triggered when the class is removed")
}

// watch for a specific class change
let classWatcher = new ClassWatcher(targetNode, 'trigger', workOnClassAdd, workOnClassRemoval)

// tests:
targetNode.classList.add('trigger') // triggers workOnClassAdd callback
targetNode.classList.add('trigger') // won't trigger (class is already exist)
targetNode.classList.add('another-class') // won't trigger (class is not watched)
targetNode.classList.remove('trigger') // triggers workOnClassRemoval callback
targetNode.classList.remove('trigger') // won't trigger (class was already removed)
targetNode.setAttribute('disabled', true) // won't trigger (the class is unchanged)

I wrapped MutationObserver with a simple class:

class ClassWatcher {

    constructor(targetNode, classToWatch, classAddedCallback, classRemovedCallback) {
        this.targetNode = targetNode
        this.classToWatch = classToWatch
        this.classAddedCallback = classAddedCallback
        this.classRemovedCallback = classRemovedCallback
        this.observer = null
        this.lastClassState = targetNode.classList.contains(this.classToWatch)

        this.init()
    }

    init() {
        this.observer = new MutationObserver(this.mutationCallback)
        this.observe()
    }

    observe() {
        this.observer.observe(this.targetNode, { attributes: true })
    }

    disconnect() {
        this.observer.disconnect()
    }

    mutationCallback = mutationsList => {
        for(let mutation of mutationsList) {
            if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                let currentClassState = mutation.target.classList.contains(this.classToWatch)
                if(this.lastClassState !== currentClassState) {
                    this.lastClassState = currentClassState
                    if(currentClassState) {
                        this.classAddedCallback()
                    }
                    else {
                        this.classRemovedCallback()
                    }
                }
            }
        }
    }
}
TechWisdom
  • 3,386
  • 4
  • 32
  • 39
  • Very nice piece of code using observer pattern! I would also highly suggest to add into mutationCallback... this.classAddedCallback() and this.classRemovedCallback() "this" into parameters of callback functions. So... this.classAddedCallback(this) and this.classRemovedCallback(this). Doing that will give easy access to specific ClassWatcher object in callback functions, therefore giving access to the targetNode and other parameters which are super useful to edit after the trigger event! – R3qUi3M Feb 11 '22 at 17:43
27

Well there were mutation events, but they were deprecated and the future there will be Mutation Observers, but they will not be fully supported for a long time. So what can you do in the mean time?

You can use a timer to check the element.

function addClassNameListener(elemId, callback) {
    var elem = document.getElementById(elemId);
    var lastClassName = elem.className;
    window.setInterval( function() {   
       var className = elem.className;
        if (className !== lastClassName) {
            callback();   
            lastClassName = className;
        }
    },10);
}

Running example: jsFiddle

epascarello
  • 195,511
  • 20
  • 184
  • 225
  • Was slammed for not referencing your answer perfectly... Anyways it can even be done using [this plugin easily](https://github.com/kapetan/jquery-observe)... My answer to a similar question is [here](http://stackoverflow.com/a/23458573/2260614).. ***Hope it helps someone*** – Bhavik May 04 '14 at 17:57
  • 7
    Mutation Observers are available these days, woooohoooo! – Ivan Š Jul 19 '16 at 12:33
  • 2
    Yah, it seems Mutation Observers is broadly supported now. https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver – MVDeveloper1 Aug 19 '20 at 12:48
19

Here's a simple, basic example on how to trigger a callback on Class attribute change
MutationObserver API

const attrObserver = new MutationObserver((mutations) => {
  mutations.forEach(mu => {
    if (mu.type !== "attributes" && mu.attributeName !== "class") return;
    console.log("class was modified!");
  });
});

const ELS_test = document.querySelectorAll(".test");
ELS_test.forEach(el => attrObserver.observe(el, {attributes: true}));


// Example of Buttons toggling several .test classNames
document.querySelectorAll(".btn").forEach(btn => {
  btn.addEventListener("click", () => ELS_test.forEach(el => el.classList.toggle(btn.dataset.class)));
});
.blue {background: blue;}
.gold {color: gold;}
<div class="test">TEST DIV</div>
<button class="btn" data-class="blue">BACKGROUND</button>
<button class="btn" data-class="gold">COLOR</button>
Roko C. Buljan
  • 180,066
  • 36
  • 283
  • 292
5

Can use this onClassChange function to watch whenever classList of an element changes

function onClassChange(element, callback) {
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (
        mutation.type === 'attributes' &&
        mutation.attributeName === 'class'
      ) {
        callback(mutation.target);
      }
    });
  });
  observer.observe(element, { attributes: true });
  return observer.disconnect;
}

var itemToWatch = document.querySelector('#item-to-watch');
onClassChange(itemToWatch, (node) => {
  node.classList.contains('active')
    ? alert('class added')
    : alert('class removed');
  node.textContent = 'Item to watch. classList: ' + node.className;
});

function addClass() {
  itemToWatch.classList.add('active');
}

function removeClass() {
  itemToWatch.classList.remove('active');
}
<div id="item-to-watch">Item to watch</div>
<button onclick="addClass();">Add Class</button>
<button onclick="removeClass();">Remove Class</button>
Loi Nguyen Huynh
  • 6,150
  • 21
  • 44
0

I needed a class update listener for a project, so I whipped this up. I didn’t end up using it, so it’s not fully tested, but should be fine on browsers supporting Element.classList DOMTokenList.

Bonus: allows “chaining” of the 4 supported methods, for example el.classList.remove(“inactive”).remove(“disabled”).add(“active”)

function ClassListListener( el ) {
  const ecl = el.classList;
  ['add','remove','toggle','replace'].forEach(prop=>{
    el.classList['_'+prop] = ecl[prop]
    el.classList[prop] = function() {
      const args = Array.from(arguments)
      this['_'+prop].apply(this, args)
      el.dispatchEvent(new CustomEvent(
        'classlistupdate',
        { detail: { method: prop, args } }
      ))
      return this
    }
  })
  return el
}

Useage:


const el = document.body

ClassListListener(el).addEventListener('classlistupdate', e => {
    const args = e.detail.args.join(', ')
    console.log('el.classList.'+e.detail.method+'('+args+')')
}, false)

el.classList
  .add('test')
  .replace('test', 'tested')