58

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
  • 13,720
  • 5
  • 57
  • 57
Patrick Jeon
  • 1,684
  • 5
  • 24
  • 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

7 Answers7

93

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,960
  • 4
  • 33
  • 40
  • 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
25

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
  • 204,599
  • 20
  • 195
  • 236
  • 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
25

Here's a simple, basic example on how to trigger a callback on Class attribute change using the 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
  • 196,159
  • 39
  • 305
  • 313
11

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
  • 8,492
  • 2
  • 29
  • 52
  • 1
    note to consider adding `itemToWatch.classList.contains('xxx')` before adding/removing, or you might be stuck in an infinite observation loop (unless you always disconnect) – Mugen Dec 28 '22 at 08:55
1

I feel like the other answers are far better, but I also have a pretty simple way using the MutationObserver:

var observer = new MutationObserver(function (mutations) {
  mutations.forEach(function (mutation) {
    if (mutation.target.classList.contains('newclass')) {
      console.log("fire at will!");
    }
  });
});

observer.observe(document.querySelector('element'), {
    attributes: true,
    attributeFilter: ['class']
});
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')
0

The idea is to substitute class manipulation functions, such as 'add', 'remove'... with wrappers, that send class change messages before or after class list changed. It's very simple to use:

  1. choose element(s) or query that selects elements, and pass it to the function.

  2. add 'class-change' and/or 'class-add', 'class-remove'... handlers to either elements or their container ('document', for example).

  3. after that, any class list change by either add, remove, replace or toggle methods will fire corresponding events.

Event sequence is:

A) 'class-change' request event is fired, that can be rejected by handler by preventDefault() if needed. If rejected, then class change will be cancelled.

B) class change function will be executed

B) 'class-add' or 'class-remove'... information event is fired.

function addClassChangeEventDispatcher( el )
{
    // select or use multiple elements
    if(typeof el === 'string') el = [...document.querySelectorAll( el )];
    
    // process multiple elements
    if(Array.isArray( el )) return el.map( addClassChangeEventDispatcher );
    
    // process single element
    
    // methods that are called by user to manipulate
    let clMethods = ['add','remove','toggle','replace'];
    
    // substitute each method of target element with wrapper that fires event after class change
    clMethods.forEach( method =>
    {
        let f = el.classList[method];       
        el.classList[method] = function() 
        {
            // prepare message info
            let detail = method == 'toggle'  ? { method, className: arguments[0] } : 
                        method == 'replace' ? { method, className: arguments[0], newClassName: arguments[1] } : 
                                                                                 { method, className: arguments[0], classNames: [...arguments] };           
            
            // fire class change request, and if rejected, cancel class operation 
            if(!el.dispatchEvent( new CustomEvent( 'class-change', {bubbles: true, cancelable: true, detail} ))) return;
            
            // call original method and then fire changed event
            f.call( this, ...arguments );
            el.dispatchEvent( new CustomEvent( 'class-' + method, {bubbles: true, detail} ));
        };
    });
    
    return el;
}
Bagration
  • 11
  • 3