I’ve written about this technique for capturing HTML elements being created before, it’s a pretty cool hack using the animation start event and a uniquely named CSS animation to catch newly created elements being added to the document.

I’ve created a small library to abstract this fairly awkward thing away and provide a simple function that can call a provided function when anything matching a CSS selector is added to the document.

It works like this.

window.addEventListener('DOMContentLoaded', function(event){
    nodeCatcher().onInsert('input.something', function(element){
        console.log(element);
    });
});

It must be used after the document has loaded, which is probably fine since you’ll most likely want to do something with the elements already on the page anyway.

Here’s the code behind that function.

var nodeCatcher = function(){

    "use strict";

    return {

        // Name and CSS property that are not likely to be used
        animationName: 'GldgSChDYIZaChFTajzM',
        animatedProperty: 'outline-color',
        animationStartValue: '#111',
        animationEndValue: '#000',

        inited: false,
        selectorMap: {},

        // https://davidwalsh.name/element-matches-selector <3
        selectorMatches: function(el, selector) {
            var p = Element.prototype;
            var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function(s){
                return [].indexOf.call(document.querySelectorAll(s), this) !== -1;
            };

            return f.call(el, selector);
        },

        onInsert: function(selector, callback){
            this.init();

            if (!this.selectorMap.hasOwnProperty(selector)){
                this.selectorMap[selector] = [];

                var style = document.createElement('style');
                style.innerHTML = selector + ' { animation-name: ' + this.animationName + '; animation-duration: 0.01s; }';
                document.head.appendChild(style);
            }

            this.selectorMap[selector].push(callback);
        },

        animationStartHandler: function(event){
            if (event.animationName !== this.animationName){
                return;
            }

            for (var selector in this.selectorMap){
                if (!this.selectorMap.hasOwnProperty(selector)){
                    continue;
                }

                if (this.selectorMatches(event.target, selector)){
                    this.selectorMap[selector].forEach(function(callback){
                        callback(event.target);
                    });

                    break;
                }
            }
        },

        init: function(){
            if (this.inited){
                return;
            }

            var style = document.createElement('style');

            var css = '@keyframes ' + this.animationName + ' { 0%: { ' + this.animatedProperty + ': ' + this.animationStartValue + '; } 100%: { ' + this.animatedProperty + ': ' + this.animationEndValue + '; } } ';
            css += '@-webkit-keyframes ' + this.animationName + ' { 0%: { ' + this.animatedProperty + ': ' + this.animationStartValue + '; } 100%: { ' + this.animatedProperty + ': ' + this.animationEndValue + '; } } ';

            style.innerHTML = css;

            document.head.appendChild(style);

            var boundHandler = this.animationStartHandler.bind(this);

            document.addEventListener('animationstart', boundHandler);
            document.addEventListener('webkitAnimationStart', boundHandler);
            document.addEventListener('MSAnimationStart', boundHandler);

            this.inited = true;
        }

    }

}

Might be of some use to somebody out there.