32

In JQuery I can do:

$(document).on("click","a.someBtn",function(e){
    console.log("hi");
});

to add an event listener to an element that doesn't exist yet. I cannot seem to figure out how to add an event listener to an element that does not exist yet in vanilla javascript.
The following does not work obviously:

query.addEventListener( "click", someListener );

Edit

What I would like to do is compare the item by query selectors. I am selecting the element that does not exist yet with querySelectorAll. It is a little more dynamic than just checking the tag name.

AmmarCSE
  • 30,079
  • 5
  • 45
  • 53
Johnston
  • 20,196
  • 18
  • 72
  • 121
  • 2
    http://stackoverflow.com/questions/14258787/add-event-listener-on-elements-created-dynamically – atmd Jun 02 '15 at 16:33
  • Can you explain a little better what you're trying to do? What do you mean by "vanilla javascript"? I think you're asking how to write a an event for an HTML element you're creating dynamically? – Kathy Jun 02 '15 at 16:33
  • "Vanilla javascript" generally mean no frameworks, although on SO it basically means no jquery – atmd Jun 02 '15 at 16:34
  • 2
    @Kathy It means no frameworks. – Johnston Jun 02 '15 at 16:42
  • @Johnston, i think the queryselector part deserves a new question. – AmmarCSE Jun 02 '15 at 16:52
  • @Johnston, but im still willing to help. I dont understand though. when are you using queryselectorall? Within the click event? Why would you compare and use queryselectorall within the click event? – AmmarCSE Jun 02 '15 at 16:53
  • @AmmarCSE The query select would go in the event listener to check if the element clicked on is the element in question. – Johnston Jun 02 '15 at 16:57
  • @Johnston, if you mean a.someBtn, why not do it the way I am in my updated answer? – AmmarCSE Jun 02 '15 at 16:58
  • @AmmarCSE That does work yes. I am just trying to get it to match any query that would be given from `querySelectorAll`. – Johnston Jun 02 '15 at 17:17
  • I'd be careful about attaching events to `document`. If you attach to many and/or the listeners you attach are too involved it could cause performance problems since ALL click events on the page will bubble up to the handler. If all of the `a.someBtn`s you are targeting have a common ancestor that is further down the DOM tree I would attach to that instead. – Useless Code Jun 03 '15 at 02:30
  • I don't like the term 'vanilla', as it confuses people. Many newcomers to Javascript will avoid this question and its answers because they think a library called Vanilla is being used and it's not for them. The term was created by pure Javascript developers as a joke and is now sadly being abused. – Martin James Jan 22 '19 at 16:24

4 Answers4

40

Use the target property in the event object to get the clicked element. Then, manually test for type/attributes/ids

document.addEventListener( "click", someListener );

function someListener(event){
    var element = event.target;
    if(element.tagName == 'A' && element.classList.contains("someBtn")){
        console.log("hi");
    }
}
AmmarCSE
  • 30,079
  • 5
  • 45
  • 53
3

You can use event.target

A reference to the object that dispatched the event.

Code

(function () {
    "use strict";
        document.getElementsByTagName('body')[0].addEventListener('click', function(e) {
        if (e.target.tagName == 'A' && e.target.classList.contains("someBtn")) {
          alert('Clicked');
        }
      }, false);
})();

(function() {
  "use strict";
  var a = document.createElement('a');
  a.textContent = 'Click Me';
  a.href = '#';
  document.body.appendChild(a);

  document.getElementsByTagName('body')[0].addEventListener('click', function(e) {
    if (e.target.tagName == 'A') {
      alert('Clicked');
    }
  }, false);
})();
Satpal
  • 132,252
  • 13
  • 159
  • 168
  • Is it possible to compare the e.target to a query selector? – Johnston Jun 02 '15 at 16:46
  • @Johnston, Yes, you can – Satpal Jun 02 '15 at 16:51
  • 1
    @Satpal, className.indexOf can fail if there happens to be a class that contains the class name searched for but not the searched class – AmmarCSE Jun 02 '15 at 17:03
  • document.getElementsByTagName('body')[0] -> document.getElementByTagName('body') -> document.body -> document. Also that self executing function is unnecessary – s123 Jun 01 '23 at 13:35
3

What you want is to use DOM MutationObserver Events to apply the addEventListener. This DOM API is available on all major browser since 2012 I think.

I use this on to lower the google translator bar created by their snippet (https://www.w3schools.com/howto/howto_google_translate.asp). Since it creates the element dynamically (an iframe), it is the same problem you have. Just change the callback function and variables for your need.

//Observer for Google translator bar creation and action to move to bottom
// Select the nodetree that will be observed for mutations
var nodetree = document.getElementsByTagName("body")[0];
// Select the target node atributes (CSS selector)
var targetNode = "iframe.goog-te-banner-frame";
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: true };
// Callback function to execute when mutations of DOM tree are observed
var lowerGoogleTranslateBar = function(mutations_on_DOMtree) {
    for(var mutation of mutations_on_DOMtree) {
        if (mutation.type == 'childList') {
            console.log(mutation);
            if (document.querySelector(targetNode) != null) {
                //40px is the height of the bar
                document.querySelector(targetNode).style.setProperty("top", "calc(100% - 40px)");
                //after action is done, disconnect the observer from the nodetree
                observerGoogleTranslator.disconnect();
            }
        }
    }
};
// Create an observer instance linked to the callback function
var observerGoogleTranslator = new MutationObserver(lowerGoogleTranslateBar);
// Start observing the target node for configured mutations
observerGoogleTranslator.observe(nodetree, config);

You can learn more about this here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

Volfegan
  • 239
  • 3
  • 5
1

Here's a function that will let you add "live" events like jQuery's .on. It can be invoked like this:

addLiveListener(scope, selector, event, function reference);

Take a look at the function comment for the description of each of those parameters.

/**
 * Adds a istener for specific tags for elements that may not yet
 * exist.
 * @param scope a reference to an element to look for elements in (i.e. document)
 * @param selector the selector in form [tag].[class] (i.e. a.someBtn)
 * @param event and event (i.e. click)
 * @param funct a function reference to execute on an event
 */
function addLiveListener(scope, selector, event, funct) {
  /**
   * Set up interval to check for new items that do not 
   * have listeners yet. This will execute every 1/10 second and
   * apply listeners to 
   */
  setInterval(function() {
    var selectorParts = selector.split('.');
    var tag = selectorParts.shift();
    var className;
    if (selectorParts.length)
      className = selectorParts.shift();

    if (tag != "") {
      tag = tag.toUpperCase();
      var elements = scope.getElementsByTagName(tag);
    } else
      var elements = scope.getElementsByClassName(className);

    for (var i = 0; i < elements.length; i++) {
      if (elements[i][event + '_processed'] === undefined && (tag == "" || elements[i].tagName == tag)) {
        elements[i].addEventListener(event, funct);
      }
    }
  }, 1000);
}

And here's a full working demo:

/**
 * Adds another anchor with no events attached and lets 
 * our other code auto-attach events
 */
var currentAnchor = 3;

function addAnchor() {
  currentAnchor++;
  var element = document.createElement('a');
  element.href = "#";
  element.innerHTML = "Anchor " + currentAnchor;
  element.className = "someBtn";
  document.getElementById("holder").appendChild(element);
}

/**
 * Adds a istener for specific tags for elements that may not yet
 * exist.
 * @param scope a reference to an element to look for elements in (i.e. document)
 * @param selector the selector in form [tag].[class] (i.e. a.someBtn)
 * @param event and event (i.e. click)
 * @param funct a function reference to execute on an event
 */
function addLiveListener(scope, selector, event, funct) {
  /**
   * Set up interval to check for new items that do not 
   * have listeners yet. This will execute every 1/10 second and
   * apply listeners to 
   */
  setInterval(function() {
    var selectorParts = selector.split('.');
    var tag = selectorParts.shift();
    var className;
    if (selectorParts.length)
      className = selectorParts.shift();

    if (tag != "") {
      tag = tag.toUpperCase();
      var elements = scope.getElementsByTagName(tag);
    } else
      var elements = scope.getElementsByClassName(className);

    for (var i = 0; i < elements.length; i++) {
      if (elements[i][event + '_processed'] === undefined && (tag == "" || elements[i].tagName == tag)) {
        elements[i].addEventListener(event, funct);
      }
    }
  }, 1000);
}

/**
 * Now let's add live listener for "a" tags
 */
addLiveListener(document, "a.someBtn", "click", function() {
  alert('Clicked ' + this.innerHTML);
});
a {
  margin-right: 10px;
}
<!-- Add some pre-existing anchors -->
<p id="holder">
  <a href="#" class="someBtn">Anchor 1</a><a href="#" class="someBtn">Anchor 2</a><a href="#" class="someBtn">Anchor 3</a>
</p>

<!-- A button to add dynamic new anchors -->
<input type="button" value="Add anchor" onclick="addAnchor();" />
martynasma
  • 8,542
  • 2
  • 28
  • 45