JavaScript attach events to elements rendered dynamically

13.2k views Asked by At

I have 2 files index.html and all.js.

all.js

(function(){
    if (document.getElementById('btn1')){
        document.getElementById("btn1").addEventListener("click", displayMessage);
    }

    function displayMessage(){
        alert("Hi!");
    }
})()

index.html

Example 1: - working example

<button id="btn1">Hit me !</button>
<script type="text/javascript" src="all.js"></script>

Example 2: - not working example

<button id="btn2" onclick="show()">Show</button>
<script type="text/javascript">
    function show(){
        var btn = document.createElement("BUTTON");
            btn.setAttribute("id", "btn1");
            btn.innerHTML = "Hit me !";
        document.getElementById("btn2");
        document.body.insertBefore(btn, btn2);
    }
</script>
<script type="text/javascript" src="all.js"></script>

Description: In the first example btn1 was rendered right away and the event was attached to it. In the second example btn1 is rendered only when you click on btn2 and after that if you click on btn1 nothing will happen.

How can I attach the event to btn1 without changing all.js?

Here is the entire code repository

Note: Don't ask my why I try to do something like this, don't ask me why I am not allowed to change all.js because this example is a simplified one, in my case all.js file contains thousands of lines with lots of events on many elements. JavaScript solution means that I am not allowed to use other external libraries.

UPDATE: Answer acceptance

I got a lot of different answers. Some of you worked harder to solve this issue, some of you worked less but the good thing is that I decided. I got some working solutions but some of them worked for only this particular case without taking in consideration that my real all.js file has 4029 lines of code, some solutions suggested to use event delegation which I agree but unfortunately I can not change my all.js file now. In the end I picked the best solutions, I've also considered who answered first, I've taken into consideration also who put a lot of work into this, etc. Anyways, the solution that I'm gonna implement is a merge between 2 solutions, Aruna's and Vlacav's solution (+ some changes by my self) and here it is:

function show(){
    var btn = document.createElement("BUTTON");
        btn.setAttribute("id", "btn1");
        btn.innerHTML = "Hit me !";
    document.getElementById("btn2");
    document.body.insertBefore(btn, btn2);
    resetJs('all.js');
}

function resetJs(path) {
    var scripts = document.getElementsByTagName('script')
        , newScript = document.createElement("script");

    newScript.src = path + "?timestamp=" + Math.round(new Date().getTime()/1000);   

    for (var i = 0; i < scripts.length; ++i) {      
        var srcUrl = scripts[i].getAttribute('src');
        if (srcUrl && srcUrl.indexOf(path) > -1) {                 
           scripts[i].parentNode.replaceChild(newScript, scripts[i]);
        }           
    }
}

Unfortunately I can't split the reputation and I have to give it to only one of them and I want to give this to Vlacav because he was the first one who posted the the solution that I was searching for and he also did some changes on it when I asked him to do so. I also rated up Aruna's answer because he deserve this.

I've posted the solution on a separate branch here and I also created several other branches on this repo with the other solutions that I received because they might be useful in other situations.

11

There are 11 answers

3
Rudolf Gröhling On BEST ANSWER

You have to reload all.js after each new element. That is the only solution I can think of.

After action that creates an element you'll call a function:

// removed, see Edit

this shall reevalute all.js and listeners from all.js shall attach to elements being in DOM.

This is very inefficient solution, so if you make several dynamic updates, reload all.js at the end of all updates, not after every single update.

This also mean, that some statefull variables from all.js might be reinitialized, counters may be reset to zero, etc.

I would personally never do this kind of piggy patching, but if you have no other choice, give it a try.

Edit

Tested working code

function reloadAllJs(){
    var path = "all.js"
        , oldScript = document.querySelector("script[src^='" + path + "']")
        , newScript = document.createElement("script")
        ;       
    // adding random get parameter to prevent caching
    newScript.src = path + "?timestamp=" + Date.now();
    oldScript.parentNode.replaceChild(newScript, oldScript);
}

querySelector and Date.now()are IE9+ compatible, for support IE8- the code must be modified.

Update

IE8- compatible version (tested in IE7)

function reloadAllJs(){
    var path = "all.js"
        , scripts = document.getElementsByTagName("script")
        , oldScript
        , newScript = document.createElement("script")
        ;

    for(var i = 0, s; s = scripts[i]; i++) {
        if (s.src.indexOf(path) !== -1) {
            oldScript = s;
            break;
        }
    }

    // adding random get parameter to prevent caching
    newScript.src = path + "?timestamp=" + (new Date()).getTime();
    oldScript.parentNode.replaceChild(newScript, oldScript);
}

Note: IE8 does not support addEventListener (it has very similar method attachEvent ), so if your all.js relies on addEventListener, then all.js is only IE9+ compatible.

6
Tolgahan Albayrak On

Use MutationObserver to listen dom changes and add events to addedNodes

function displayMessage(){
  alert('Hi!');
}

function show(){
  var btn = document.createElement("BUTTON");
  btn.setAttribute("id", "btn1");
  btn.innerHTML = "Hit me !";
  document.getElementById("btn2");
  document.body.insertBefore(btn, btn2);
}

(new MutationObserver(mutations => {
  mutations
    .reduce((prev, cur) => prev.concat(Array.from(cur.addedNodes)), [])
    .filter(node => node.id === "btn1")
    .forEach(button => button.addEventListener('click', displayMessage));
})).observe(document.body, {childList: true, subtree: true})
<button id="btn2" onclick="show()">Show</button>

UPDATE: Monkey Patch!

// your hack

// listen for all nodes
(new MutationObserver(mutations => {
  mutations
    .reduce((prev, cur) => prev.concat(Array.from(cur.addedNodes)), [])
    .forEach(replicateEvents);
})).observe(document.body, {childList: true, subtree: true})

// override addEventListener
const initialEvents = {};
const oldAddListener = Element.prototype.addEventListener;
Element.prototype.addEventListener = function(type, cb) {
  if(this.id) {
    var events = initialEvents[this.id] || (initialEvents[this.id] = {types: {}});
    if(!events.types[type]){
      events.target = this;
      events.types[type] = [cb];
    } else if(events.target === this) {
      events.types[type].push(cb);
    }
  }
  oldAddListener.apply(this, Array.from(arguments));
};

function replicateEvents(node){
  let events = initialEvents[node.id];
  if(!events) return;
  Object.keys(events.types)
  .forEach(type => events.types[type].forEach(cb => oldAddListener.call(node, type, cb)));
}


function show(){
  var btn = document.createElement("BUTTON");
  btn.setAttribute("id", "btn1");
  btn.innerHTML = "Hit me !";
  document.getElementById("btn2");
  document.body.insertBefore(btn, btn2);
}



// all.js

(function(){
    if (document.getElementById('btn1')){
        document.getElementById("btn1").addEventListener("click", displayMessage);
        document.getElementById("btn1").addEventListener("click", displayMessage2);
    }

    function displayMessage(){
        alert("Hi!");
    }

    function displayMessage2(){
      console.log("Hi2!");
    }
})()
<button id="btn2" onclick="show()">Show</button>
<br />
<button id="btn1">Some initial button</button>

1
user2182349 On

You should also be able to use event delegation.

If the new DOM elements are all placed in a div with an id of "dynamic", you can use the following:

document.getElementById("dynamic").addEventListener("click",function(event){
    var target = event.target;
    console.log(target); 
});

This will capture all the click events that occur in the dynamic div and you can test the event target to determine which element was clicked. You only need one event handler with this approach.

1
Kiran Kumar On

In your case displayMessage() btn1 handler will be trying to register first before the btn1 getting registered with DOM, when user clicks on btn2 the btn1 element will be registered with the DOM. So there won't be any handler to trigger. So, Please register the handler after registering the btn1 like below.

<button id="btn2" onclick="show()">Show</button>
<script type="text/javascript">
function show(){
    var btn = document.createElement("BUTTON");
        btn.setAttribute("id", "btn1");
        btn.innerHTML = "Hit me !";
    document.getElementById("btn2");
    document.body.insertBefore(btn, btn2);
    btn.addEventListener("click", displayMessage);
}
</script>
<script type="text/javascript" src="all.js"></script>
2
Jeanger On

Add an onclick attribute to the element btn.

function displayMessage() {
  alert("Hi!");
}

function show() {
  var btn = document.createElement("BUTTON");
  btn.setAttribute("id", "btn1");
  btn.innerHTML = "Hit me !";
  btn.onclick = displayMessage;
  document.getElementById("btn2");
  document.body.insertBefore(btn, btn2);
}
<button id="btn2" onclick="show()">Show</button>

8
Aruna On

At the time of all.js is being loaded, it's looking for the element btn1 and trying to attach the event if it presents. As the element is created/loaded dynamically, it's not getting attached to the event when all.js is loaded and so the element has no event attached to this after it is being created.

As per the discussion I had with you, I understood the following things from you:

  • You can't change the all.js and index.html as both are maintained by your client and protected.
  • index.html is calling the method something like initHeader() which in turn calls the webservice with ajax request.
  • And then index.html is parsing the json returned by the ajax call which is actually you return and can have html and js inside.
  • This parsed json (html) is then appended to the page like, document.getElementByID('nav').innerHtml = ajaxResponse;

Based on the above understandings, the only place you can change/do something is the content which you returned from the webservice.

So I can suggest you the below script which you can add inside your webservice response at the end which will remove the existing all.js file from index.html and load the same once again after the ajax response.

<script type="text/javascript">
    function reattachEvents() {
      // Remove 'all.js'
      var scripts = document.getElementsByTagName('script')
      for (var i = scripts.length - 1; i >= 0; i--){ // Loop backwards
         if (scripts[i]) {
            var srcUrl = scripts[i].getAttribute('src');
            if(srcUrl && srcUrl.indexOf('all.js') > -1) {
               scripts[i].parentNode.removeChild(scripts[i]);
            }
         }
      }

      // Load 'all.js'
      var head= document.getElementsByTagName('head')[0];
      var script= document.createElement('script');
      script.type= 'text/javascript';
      script.src= 'all.js';
      head.appendChild(script);
   }

   reattachEvents();
</script>

Note: I have created a new pull request in your repository with this changes

6
Aabir Hussain On

PROBLEM WITH CODE:

The problem is that you are using addEventListener in a block which is executed when all.js file is loaded. So, It will add EventListener for all the ID's ( btn1 ) which are present in the page before all.js get called. When you are calling show() to add new element with id btn1, all.js is not able to add EventListner for this element.

So, If you are adding new element then you again have to use addEventListener.

SOLUTION 1:

use addEventListner after element inserted into body

function show() {
  var btn = document.createElement("BUTTON");
  btn.setAttribute("id", "btn1");
  btn.innerHTML = "Hit me !";
 //  btn.onclick = displayMessage;  // you can use this also.
  document.getElementById("btn2");
  document.body.insertBefore(btn, btn2);
//document.getElementById("btn1").addEventListener("click", displayMessage); //like this
}

I don't think you are looking for this solution.

SOLUTION 2: If you are also using JQuery Library, then it will be easy but you are also use pure javascript.

You just have to reload your all.js after inserting elements.

Below is your index2.html with perfect solution with jquery. if you do not have Jquery then reload your all.js with javascript.

<html>
<body>
<button id="btn2" onclick="show()">Show</button>
<script type="text/javascript" src="../jquery.js"></script>
<script type="text/javascript">
    function show(){
        var btn = document.createElement("BUTTON");
            btn.setAttribute("id", "btn1");
            btn.innerHTML = "Hit me !";
        document.getElementById("btn2");
        document.body.insertBefore(btn, btn2);

        //HERE YOU HAVE TO RELOAD YOUR all.js
        reload_js('all.js');    
    }
</script>
<script type="text/javascript">
    function reload_js(src) {
            $('script[src="' + src + '"]').remove();
            $('<script>').attr('src', src).appendTo('head');
        }
</script>
<script type="text/javascript" src="all.js"></script>
</body>
</html>

EDIT Only Javascript

   <html>
<body>
<button id="btn2" onclick="show()">Show</button>
<script type="text/javascript">
    function show(){
        var btn = document.createElement("BUTTON");
            btn.setAttribute("id", "btn1");
            btn.innerHTML = "Hit me !";
        document.getElementById("btn2");
        document.body.insertBefore(btn, btn2);
        //Pass the name you want to reload 
        loadJs('all.js');
    }
</script>
<script type="text/javascript" src="all.js"></script>


<script type="text/javascript">


     function loadJs(js)
       {    
            //selecting js file
            var jsSection = document.querySelector('script[src="'+js+'"]');

            //selecting parent node to remove js and add new js 
            var parent = jsSection.parentElement;
            //deleting js file
            parent.removeChild(jsSection);

            //creating new js file 
            var script= document.createElement('script');
            script.type= 'text/javascript';
            script.src= js;

            //adding new file in selected tag.
            parent.appendChild(script);
       }
</script>
</body>
</html>

For more options to reload. See how to reload javascript files here

Here is the update repository

0
Azad On

create a method to attach the event listner into button in all.js file

   function attachClickEvent(elementId, event){
      var element = document.getElementById(elementId);
      if (element ){
        element.addEventListener("click", event);
      }    
    }

    function displayMessage(){
        alert("Hi!");
    }

index.html

<button id="btn2" onclick="show()">Show</button>
<script type="text/javascript">
    function show(){
        var btn = document.createElement("BUTTON");
            btn.setAttribute("id", "btn1");
            btn.innerHTML = "Hit me !";
        document.getElementById("btn2");
        document.body.insertBefore(btn, btn2);

        //now call attachClickEvent function to add event listner to btn1
        //(both displayMessage() and attachClickEvent() functions are loaded now)
        attachClickEvent('btn1', displayMessage);
    }
</script>
<script type="text/javascript" src="all.js"></script>
5
Jeremy Klein On

You could use jQuery to load the script each time the button is pressed

<button id="btn2" onclick="show()">Show</button>
<script
  src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script type="text/javascript">
    function show(){
        var btn = document.createElement("BUTTON");
            btn.setAttribute("id", "btn1");
            btn.innerHTML = "Hit me !";
        document.getElementById("btn2");
        document.body.insertBefore(btn, btn2);
        jQuery.getScript("all.js");
    }
</script>
0
Sanjay Nishad On

I think you should create element on initial load either in JS or HTML

Please check these examples.

Example 1:

<button id="btn2" onclick="show()">Show</button>
<script type="text/javascript">
  function initHiddenButton(id, text) {
    var btn = document.createElement("BUTTON");
    btn.setAttribute("id", id);
    btn.innerHTML = text;
    btn.style.display = 'none';
    document.body.appendChild(btn);
  }
  initHiddenButton("btn1", "Hit me !");
    function show(){
      var btn1 = document.getElementById("btn1");
      var btn2 = document.getElementById("btn2");
        btn1.style.display = 'inline-block';
        document.body.insertBefore(btn1, btn2);

        //others code...
    }
</script>
<script src="all.js"></script>

Example 2:

<button id="btn2" onclick="show()">Show</button>

<!-- hidden buttons -->
<button id="btn1" onclick="show()" style="display: none;">Hit me !</button>

<script type="text/javascript">
    function show(){
      var btn1 = document.getElementById("btn1");
      var btn2 = document.getElementById("btn2");
        btn1.style.display = 'inline-block';
        document.body.insertBefore(btn1, btn2);

        //others code...
    }
</script>
<script src="all.js"></script>
2
Hakier On

Using pure js You can do it like this:

document.querySelector('body').addEventListener('click', function (event) {
    if (event.target.id !== 'btn1') {
        return; //other element than #btn1 has been clicked
    }

    //Do some stuff after clicking #btn1
});

If you can use jQuery it has method "on" which can be called like below

$('body').on('click', '#btn1', function(event) {
    // do something when #btn has been clicked
});

You are binding on click listener to body (or any other element that contains #btn1) and if it has been trigered then second argument filters if it has been trigered from #btn1, if not it will not fire callback.

In that case element matching selector in second argument of "on" method does not have to exist when watcher is registered.

You can read more about it in jQuery on documentation

Happy hacking!