168

In the following example code, I attach an onclick event handler to the span containing the text "foo". The handler is an anonymous function that pops up an alert().

However, if I assign to the parent node's innerHTML, this onclick event handler gets destroyed - clicking "foo" fails to pop up the alert box.

Is this fixable?

<html>
 <head>
 <script type="text/javascript">

  function start () {
    myspan = document.getElementById("myspan");
    myspan.onclick = function() { alert ("hi"); };

    mydiv = document.getElementById("mydiv");
    mydiv.innerHTML += "bar";
  }

 </script>
 </head>

 <body onload="start()">
   <div id="mydiv" style="border: solid red 2px">
     <span id="myspan">foo</span>
   </div>
 </body>

</html>
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
mike
  • 46,876
  • 44
  • 102
  • 112
  • This question is related to the 'appending new fields to a form erases user input' problem. The selected answer fixes both of these problems very nicely. – MattT Jul 24 '14 at 18:02
  • Event delegation can be used to tackle this problem. – dasfdsa Sep 28 '19 at 08:33

13 Answers13

178

Using .insertAdjacentHTML() preserves event listeners, and is supported by all major browsers. It's a simple one-line replacement for .innerHTML.

var html_to_insert = "<p>New paragraph</p>";

// with .innerHTML, destroys event listeners
document.getElementById('mydiv').innerHTML += html_to_insert;

// with .insertAdjacentHTML, preserves event listeners
document.getElementById('mydiv').insertAdjacentHTML('beforeend', html_to_insert);

The 'beforeend' argument specifies where in the element to insert the HTML content. Options are 'beforebegin', 'afterbegin', 'beforeend', and 'afterend'. Their corresponding locations are:

<!-- beforebegin -->
<div id="mydiv">
  <!-- afterbegin -->
  <p>Existing content in #mydiv</p>
  <!-- beforeend -->
</div>
<!-- afterend -->
Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
Josh
  • 2,105
  • 1
  • 14
  • 14
  • 1
    be careful Chrome / Edge complains about case sensitivity: i.e. `afterbegin` is not accepted `afterBegin` is! – AK_ Jan 19 '21 at 22:24
  • 2
    @AK_ The [specification](//w3c.github.io/DOM-Parsing/#dom-element-insertadjacenthtml) guarantees that the string match is case-insensitive. – Sebastian Simon Mar 22 '22 at 23:12
158

Unfortunately, assignment to innerHTML causes the destruction of all child elements, even if you're trying to append. If you want to preserve child nodes (and their event handlers), you'll need to use DOM functions:

function start() {
    var myspan = document.getElementById("myspan");
    myspan.onclick = function() { alert ("hi"); };

    var mydiv = document.getElementById("mydiv");
    mydiv.appendChild(document.createTextNode("bar"));
}

Edit: Bob's solution, from the comments. Post your answer, Bob! Get credit for it. :-)

function start() {
    var myspan = document.getElementById("myspan");
    myspan.onclick = function() { alert ("hi"); };

    var mydiv = document.getElementById("mydiv");
    var newcontent = document.createElement('div');
    newcontent.innerHTML = "bar";

    while (newcontent.firstChild) {
        mydiv.appendChild(newcontent.firstChild);
    }
}
Ben Blank
  • 54,908
  • 28
  • 127
  • 156
  • Is there a substitute that can append an arbitrary blob of HTML? – mike Feb 27 '09 at 17:55
  • 84
    newcontent= document.createElement('div'); newcontent.innerHTML= arbitrary_blob; while (newcontent.firstChild) mydiv.appendChild(newcontent.firstChild); – bobince Feb 27 '09 at 18:13
  • Nice, Bob! If you post that as a well-formatted answer, I'll select it. – mike Feb 27 '09 at 18:17
  • @bobince — I've updated my answer with your technique, since you haven't posted it yet yourself. If you create your own answer, go ahead and roll mine back. :-) – Ben Blank Feb 27 '09 at 20:25
  • @Ben: you don't need the call to newcontent.removeChild(). Bob's original comment was correct. – Crescent Fresh Feb 27 '09 at 20:33
  • @crescentfresh — If you don't remove the child node from newcontent, `while (newcontent.firstChild)` loops forever. Try it. – Ben Blank Feb 27 '09 at 21:01
  • Hmm… I take that back. It *shouldn't* loop — I have no idea what was going wrong the first time I tested it. :-/ – Ben Blank Feb 27 '09 at 21:09
  • 2
    Oh, one last thing, you'll want “var myspan”, “var newcontent” etc. to avoid accidentally spilling globals. – bobince Feb 27 '09 at 21:24
  • Heh. It's becoming apparent that my tendency to bash out quick & dirty example/prototype code doesn't serve me well on SO. :-) – Ben Blank Feb 27 '09 at 21:58
  • `firstChild` didn't work as expected with me, but `firstElementChild` did. – Omar Jun 18 '20 at 18:15
  • @Omar How did it not work as expected? A construct with a loop like `const newContent = Object.assign(document.createElement("div"), { innerHTML: arbitraryBlob }); while(newContent.firstChild){ mydiv.appendChild(newcontent.firstChild); }` is expected to work. `newContent.firstChild` is either a Node or `null`. `appendChild` moves a Node — Nodes can’t exist at two places at once. So the loop eventually stops when `newContent.firstChild === null`; at this point, all child nodes of `newContent` have been moved to the target `mydiv` — without affecting any existing child nodes. – Sebastian Simon Mar 22 '22 at 23:22
3

I created my markup to insert as a string since it's less code and easier to read than working with the fancy dom stuff.

Then I made it innerHTML of a temporary element just so I could take the one and only child of that element and attach to the body.

var html = '<div>';
html += 'Hello div!';
html += '</div>';

var tempElement = document.createElement('div');
tempElement.innerHTML = html;
document.getElementsByTagName('body')[0].appendChild(tempElement.firstChild);
Eneroth3
  • 47
  • 1
3

Now, it is 2012, and jQuery has append and prepend functions that do exactly this, add content without effecting current content. Very useful.

rocketsarefast
  • 4,072
  • 1
  • 24
  • 18
  • 15
    [`.insertAdjacentHTML`](https://developer.mozilla.org/en/DOM/element.insertAdjacentHTML) has been around since IE4 – Esailija Jul 23 '12 at 13:47
  • 1
    @Tynach yes it does, it's their JavaScript site that documents it the best: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML – Jordon Bedwell Nov 25 '16 at 14:37
  • 2
    @JordonBedwell, I honestly have no idea why I said what I did before. You're 100% right. I feel like at the time I briefly looked into it and couldn't find it, but... Honestly I have no excuse. Esailija even *links* to that page. – Tynach Sep 09 '17 at 03:15
  • 2
    OP is not talking about jQuery –  Oct 15 '19 at 19:52
  • 1
    [jQuery meme fun](https://www.doxdesk.com/img/updates/20091116-so-large.gif) – Ruan Mendes Aug 23 '21 at 12:10
1

There is another alternative: using setAttribute rather than adding an event listener. Like this:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Demo innerHTML and event listeners</title>
<style>
    div {
        border: 1px solid black;
        padding: 10px;
    }
</style>
</head>
<body>
    <div>
        <span>Click here.</span>
    </div>
    <script>
        document.querySelector('span').setAttribute("onclick","alert('Hi.')");
        document.querySelector('div').innerHTML += ' Added text.';
    </script>
</body>
</html>
1

Yes it is possible if you bind events using tag attribute onclick="sayHi()" directly in template similar like your <body onload="start()"> - this approach similar to frameworks angular/vue/react/etc. You can also use <template> to operate on 'dynamic' html like here. It is not strict unobtrusive js however it is acceptable for small projects

function start() {
  mydiv.innerHTML += "bar";
}

function sayHi() {
  alert("hi");
}
<body onload="start()">
  <div id="mydiv" style="border: solid red 2px">
    <span id="myspan" onclick="sayHi()">foo</span>
  </div>
</body>
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
1

As a slight (but related) asside, if you use a javascript library such as jquery (v1.3) to do your dom manipulation you can make use of live events whereby you set up a handler like:

 $("#myspan").live("click", function(){
  alert('hi');
});

and it will be applied to that selector at all times during any kind of jquery manipulation. For live events see: docs.jquery.com/events/live for jquery manipulation see: docs.jquery.com/manipulation

Cargowire
  • 1,488
  • 10
  • 21
0

The easiest way is to use an array and push elements into it and then insert the array subsequent values into the array dynamically. Here is my code:

var namesArray = [];

function myclick(){
    var readhere = prompt ("Insert value");
    namesArray.push(readhere);
    document.getElementById('demo').innerHTML= namesArray;
}
0

Losing event handlers is, IMO, a bug in the way Javascript handles the DOM. To avoid this behavior, you can add the following:

function start () {
  myspan = document.getElementById("myspan");
  myspan.onclick = function() { alert ("hi"); };

  mydiv = document.getElementById("mydiv");
  clickHandler = mydiv.onclick;  // add
  mydiv.innerHTML += "bar";
  mydiv.onclick = clickHandler;  // add
}
Yes - that Jake.
  • 16,725
  • 14
  • 70
  • 96
  • 2
    I don't consider this a bug. Replacing an element means you completely replace it. It should not inherit what was there before. – Diodeus - James MacFarlane Feb 27 '09 at 17:51
  • But I don't want to replace an element; I just want to append new ones to the parent. – mike Feb 27 '09 at 17:53
  • If you were replacing the element, I would agree, @Diodeus. It's not the .innerHTML that has the event handler, but the .innerHtml's parent. – Yes - that Jake. Feb 27 '09 at 17:57
  • 2
    The innerHTML's owner does not lose handlers when its innerHTML is changed, only anything in its contents. And JavaScript doesn't know that you are only appending new children, since “x.innerHTML+= y” is only syntactical sugar for the string operation “x.innerHTML= x.innerHTML+y”. – bobince Feb 27 '09 at 18:16
  • You don't need to re-attach `mydiv.onclick`. Only the inner span's onclick is overridden by `innerHTML +=`. – Crescent Fresh Feb 27 '09 at 20:35
-1

You could do it like this:

var anchors = document.getElementsByTagName('a'); 
var index_a = 0;
var uls = document.getElementsByTagName('UL'); 
window.onload=function()          {alert(anchors.length);};
for(var i=0 ; i<uls.length;  i++)
{
    lis = uls[i].getElementsByTagName('LI');
    for(var j=0 ;j<lis.length;j++)
    {
        var first = lis[j].innerHTML; 
        string = "<img src=\"http://g.etfv.co/" +  anchors[index_a++] + 
            "\"  width=\"32\" 
        height=\"32\" />   " + first;
        lis[j].innerHTML = string;
    }
}
Gorgsenegger
  • 7,356
  • 4
  • 51
  • 89
-1

something.innerHTML += 'add whatever you want';

it worked for me. I added a button to an input text using this solution

KawaiKx
  • 9,558
  • 19
  • 72
  • 111
  • 10
    It destroys all events. – TuralAsgar Nov 06 '19 at 20:30
  • 4
    how is it really a solution? it destroys all the events!! – Danish Nov 20 '19 at 12:30
  • and if you use web components it probably calls connected callbacks. essentially you re set the html, and all of the children are brand new elements. thus, no internal state (if they're not in the attributes) is preserved. also, it removes focus and deletes all the text you have inserted in lets say textarea. so, it should re render it all from your string. don't do it or avoid as much as possible. – FLAW Oct 29 '22 at 09:06
-1

For any object array with header and data.jsfiddle

https://jsfiddle.net/AmrendraKumar/9ac75Lg0/2/

<table id="myTable" border='1|1'></table>

<script>
  const userObjectArray = [{
    name: "Ajay",
    age: 27,
    height: 5.10,
    address: "Bangalore"
  }, {
    name: "Vijay",
    age: 24,
    height: 5.10,
    address: "Bangalore"
  }, {
    name: "Dinesh",
    age: 27,
    height: 5.10,
    address: "Bangalore"
  }];
  const headers = Object.keys(userObjectArray[0]);
  var tr1 = document.createElement('tr');
  var htmlHeaderStr = '';
  for (let i = 0; i < headers.length; i++) {
    htmlHeaderStr += "<th>" + headers[i] + "</th>"
  }
  tr1.innerHTML = htmlHeaderStr;
  document.getElementById('myTable').appendChild(tr1);

  for (var j = 0; j < userObjectArray.length; j++) {
    var tr = document.createElement('tr');
    var htmlDataString = '';
    for (var k = 0; k < headers.length; k++) {
      htmlDataString += "<td>" + userObjectArray[j][headers[k]] + "</td>"
    }
    tr.innerHTML = htmlDataString;
    document.getElementById('myTable').appendChild(tr);
  }

</script>
AKS
  • 51
  • 5
-7

I'm a lazy programmer. I don't use DOM because it seems like extra typing. To me, the less code the better. Here's how I would add "bar" without replacing "foo":

function start(){
var innermyspan = document.getElementById("myspan").innerHTML;
document.getElementById("myspan").innerHTML=innermyspan+"bar";
}
lucious
  • 29
  • 1
  • 16
    This question is asking how to do this without losing event handlers, so your answer is not relevant, and btw this is what += does, which is way shorter – wlf Nov 23 '12 at 10:45
  • 6
    It's hilarious that in this completely wrong answer that cites avoiding extra typing as a justification, about half the code is redundant. – JLRishe Oct 20 '17 at 19:17