10

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.

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
paulalexandru
  • 9,218
  • 7
  • 66
  • 94
  • @Mahi - As I said in the note section, my real all.js file has thousands of lines. I can't rewrite it again, I must use all.js. – paulalexandru Dec 16 '16 at 19:15
  • i am suggesting you to write that code into script tag not all.js file – Mahi Dec 16 '16 at 19:18
  • @Mahi - I can't because of at least 3 reasons. First because inline code may slow the loading time and secondly because this file is a bundle js file which may contain a lot of other javascript code which are not events. And the last thing is that this is handeld by 2 different teams, one team adds the all.js file and nother one should handle the dynamic part when the html is rendered. – paulalexandru Dec 16 '16 at 19:23
  • @Mahi - I tried also putting all the all.js content into a script tag just to see what happens, but it seem that it doesn't work like that also. – paulalexandru Dec 16 '16 at 19:25
  • @paulalexandru check my answer (update section) – Tolgahan Albayrak Dec 16 '16 at 20:19
  • 3
    This is against good design principles. `all.js` simply does not support attaching `displayMessage` to `#btn1` after the script has loaded. Something like the solution by Tolgahan Albayrak may work, but there shouldn't be a need to do such a thing in the first place. If team 1 is writing `all.js` and team 2 is writing `function show()`, then the teams are not cooperating. – Julian Dec 18 '16 at 23:45
  • @paulalexandru I have given a working answer below without changing the `all.js` file as you expected. You can have a look and let me know if you need any modifications/fixes further. – Aruna Dec 22 '16 at 05:19
  • @Aruna - thanks, I will try it out and come back to you if it's the case. – paulalexandru Dec 22 '16 at 06:18
  • @paulalexandru Sure :-) – Aruna Dec 22 '16 at 06:26
  • @paulalexandru Did you get a chance to look at my answer? – Aruna Dec 23 '16 at 19:59
  • @Aruna I was a little busy in the past days but I will test all the answers and I will pick which fits the best for me. – paulalexandru Dec 23 '16 at 21:05
  • Sure @paulalexandru – Aruna Dec 23 '16 at 21:23
  • did you try attaching the eventListener inside the `show()` function after the element creation? then getting rid of the one inside `all.js`. – Anthony Dec 24 '16 at 02:30

11 Answers11

3

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>
Jeanger
  • 92
  • 4
  • This is a particular solution. As I wrote in the question, my all.js file has many lines of code in fact, 4029 lines exactly. So there are lots of events there, I can't move all my events from all.js inside index.html, I can't move all my javascript inside my html file. I must somehow reuse all.js because in your example, all.js is not used anymore. You should think more generally about this solution, not just to make a simple hack to solve the single event I wrote down here for an example. – paulalexandru Dec 22 '16 at 06:23
  • I think this is not the solution @paulalexandru looking for. He can't use `displayMessage` in his code because there may be lots of functions like `displayMessage' that he need to call in html file. – Aabir Hussain Dec 24 '16 at 05:35
2

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

Community
  • 1
  • 1
Aabir Hussain
  • 1,161
  • 1
  • 11
  • 29
  • The solution 2 seems nice at a first look, the problem is that I can't use external libraries like jQuery. – paulalexandru Dec 23 '16 at 11:50
  • as I understood your problem is you can't use `displayMessage` in your html file or you have many function that you can't call again in you html file. So, you have have to reload `all.js` file. – Aabir Hussain Dec 23 '16 at 12:13
  • This might me the solution that I search for, the only thing is that my all.js file is not placed inside a div with an id, so I can't apply this solution only if you have a solution with a div. – paulalexandru Dec 23 '16 at 21:49
  • Please fork the repo, dupplicate index2.html into index3.html and add your modifications there, make sure it works and ask for a pull request and I will check your code there. (also update your code here) – paulalexandru Dec 24 '16 at 01:14
  • And I also need to append the script exactly in the same spot from where you delete it. If you have a solution for this, it's more probably you will get the reputation. – paulalexandru Dec 24 '16 at 01:40
  • m not able to write in your repo. So, I have created new here https://github.com/Aabir1/Event/tree/patch-1. And updated code will add and remove js file in the same Parent.Hope this would help you. Check index3.html – Aabir Hussain Dec 24 '16 at 05:23
1

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.

user2182349
  • 9,569
  • 3
  • 29
  • 41
  • Event delegation is a good thing, but this means I have to rewrite all my js now and it is not something that I can do considering that in reality all.js file has 4029 lines of code. – paulalexandru Dec 22 '16 at 06:27
1

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

Aruna
  • 11,959
  • 3
  • 28
  • 42
  • Please clone the repo, create a new branch add your modifications in index2.html only, make sure it works and ask for a pull request and I will check your code there. – paulalexandru Dec 23 '16 at 22:02
  • @paulalexandru I cloned the repo but I don't have access to create a branch and check in my code. I have added a new page `index3.html`. Can you give me access to the repo to check in? My profile url is, https://github.com/ArunaBalavijay – Aruna Dec 24 '16 at 00:45
  • Mabey you need to fork it and ask for a pull request ? – paulalexandru Dec 24 '16 at 00:50
  • Yes, I did the same and can you please check? – Aruna Dec 24 '16 at 00:54
  • I checked it, it is working. Can you please explain it to me, what is the process here? – paulalexandru Dec 24 '16 at 01:08
  • @paulalexandru Sure – Aruna Dec 24 '16 at 01:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/131387/discussion-between-aruna-and-paulalexandru). – Aruna Dec 24 '16 at 01:12
  • @paulalexandru As per our discussion, I have updated my answer and created a pull request. Please read the updated answer once again. – Aruna Dec 24 '16 at 11:19
1

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.

Rudolf Gröhling
  • 4,611
  • 4
  • 27
  • 37
  • I tried the second function and it is not working. I get an error: SyntaxError: An invalid or illegal string was specified. – paulalexandru Dec 23 '16 at 21:45
  • You can fork the repository showed in the question, the master version, add your code in index2.html file to see the error and fix it. If you fix this, it might me the solution I am searching for. – paulalexandru Dec 24 '16 at 09:49
  • Hey Vaclav, your code looks nice, the thing is that I must support Ie8- also. If you want to post an update, please don't delete the IE9+ version because it might be useful for others. – paulalexandru Dec 24 '16 at 19:52
0

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>
  • Hi, I try not to use external libraries and achieve this only with pure js. – paulalexandru Dec 23 '16 at 08:04
  • 1
    It looks like this sniping could work for you. https://gist.github.com/askesian/5711959 – Jeremy Klein Dec 23 '16 at 11:24
  • Yes, something like that, the only thing is that example does reload the script by adding it again and not by replacing it, so I will end up have 2 same scripts tags in the page. – paulalexandru Dec 23 '16 at 21:55
  • Without knowing more about the script that you are trying to run, I must ask.. is hving 2 of the same script tags on the page an issue? If so, you could likely modify the gist that I posted above to remove the script tag if it exists. I'll be happy to take a crack at that later tonight. – Jeremy Klein Dec 23 '16 at 22:07
  • Yes, doesn't seem very pro to have 2 same scripts on the page. Unfortunetly I can't use jQuery. – paulalexandru Dec 23 '16 at 22:13
0

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>
Sanjay Nishad
  • 1,537
  • 14
  • 26
0

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!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Hakier
  • 505
  • 2
  • 13
  • Hi. I can't use external libraries like jQuery. The thing with event delegation is that it forces me to rewrite my all.js file which I can't do in this case. – paulalexandru Dec 23 '16 at 11:48
  • I have updated answer to contain also solution for bare javascript – Hakier Dec 23 '16 at 13:05
-1

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>
Tolgahan Albayrak
  • 3,118
  • 1
  • 25
  • 28
  • How is this done in case I have like 1000 buttons and each one has like 2,3 events attached to them? Can I reuse all.js file or that is useless in example 2? – paulalexandru Dec 16 '16 at 19:34
  • `btn.setAttribute("onclick", "displayMessage()");` ?? would do the same – Mahi Dec 16 '16 at 19:38
  • @paulalexandru Update works for all elements and events – Tolgahan Albayrak Dec 16 '16 at 20:12
  • I will try to test your code, it is very likely to crash at all.js since inside it there is lots of lines of code and different event types, but in case it will work, I will come back to you and mark your question as useful. I thought that there is a way somehow to rerender all.js in some way in order to keep and use the same events. – paulalexandru Dec 16 '16 at 20:50
  • since original event handlers are scoped by a parent function (as mentioned in your example) there is no other way to do that – Tolgahan Albayrak Dec 16 '16 at 21:02
  • Please, please, please, and please! The downvoter! Could you please give me a particular reason for your down vote ? – Tolgahan Albayrak Dec 23 '16 at 20:30
-1

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>
Kiran Kumar
  • 1,033
  • 7
  • 20
-1

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>
Azad
  • 5,144
  • 4
  • 28
  • 56