Is it possible to append to innerHTML without destroying descendants' event listeners?
Asked Answered
E

14

172

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>
Eristic answered 27/2, 2009 at 17:43 Comment(2)
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.Vanmeter
Event delegation can be used to tackle this problem.Jacquiline
S
160

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);
    }
}
Seasonseasonable answered 27/2, 2009 at 17:47 Comment(11)
Is there a substitute that can append an arbitrary blob of HTML?Eristic
newcontent= document.createElement('div'); newcontent.innerHTML= arbitrary_blob; while (newcontent.firstChild) mydiv.appendChild(newcontent.firstChild);Thiazine
Nice, Bob! If you post that as a well-formatted answer, I'll select it.Eristic
@Thiazine — 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. :-)Seasonseasonable
@Ben: you don't need the call to newcontent.removeChild(). Bob's original comment was correct.Peterson
@crescentfresh — If you don't remove the child node from newcontent, while (newcontent.firstChild) loops forever. Try it.Seasonseasonable
Hmm… I take that back. It shouldn't loop — I have no idea what was going wrong the first time I tested it. :-/Seasonseasonable
Oh, one last thing, you'll want “var myspan”, “var newcontent” etc. to avoid accidentally spilling globals.Thiazine
Heh. It's becoming apparent that my tendency to bash out quick & dirty example/prototype code doesn't serve me well on SO. :-)Seasonseasonable
firstChild didn't work as expected with me, but firstElementChild did.Artamas
@Artamas 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.Ocko
D
190

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 -->
Dyal answered 7/12, 2016 at 16:35 Comment(2)
be careful Chrome / Edge complains about case sensitivity: i.e. afterbegin is not accepted afterBegin is!Medlar
@Medlar The specification guarantees that the string match is case-insensitive.Ocko
S
160

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);
    }
}
Seasonseasonable answered 27/2, 2009 at 17:47 Comment(11)
Is there a substitute that can append an arbitrary blob of HTML?Eristic
newcontent= document.createElement('div'); newcontent.innerHTML= arbitrary_blob; while (newcontent.firstChild) mydiv.appendChild(newcontent.firstChild);Thiazine
Nice, Bob! If you post that as a well-formatted answer, I'll select it.Eristic
@Thiazine — 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. :-)Seasonseasonable
@Ben: you don't need the call to newcontent.removeChild(). Bob's original comment was correct.Peterson
@crescentfresh — If you don't remove the child node from newcontent, while (newcontent.firstChild) loops forever. Try it.Seasonseasonable
Hmm… I take that back. It shouldn't loop — I have no idea what was going wrong the first time I tested it. :-/Seasonseasonable
Oh, one last thing, you'll want “var myspan”, “var newcontent” etc. to avoid accidentally spilling globals.Thiazine
Heh. It's becoming apparent that my tendency to bash out quick & dirty example/prototype code doesn't serve me well on SO. :-)Seasonseasonable
firstChild didn't work as expected with me, but firstElementChild did.Artamas
@Artamas 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.Ocko
D
4

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

Defamatory answered 20/1, 2012 at 18:6 Comment(5)
.insertAdjacentHTML has been around since IE4Watersick
@Tynach yes it does, it's their JavaScript site that documents it the best: developer.mozilla.org/en-US/docs/Web/API/Element/…Mammy
@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.Chill
OP is not talking about jQuerySemolina
jQuery meme funPharyngoscope
G
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);
Gormley answered 4/7, 2012 at 8:8 Comment(0)
R
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

Regalia answered 28/2, 2009 at 13:5 Comment(1)
OP is not talking about jQuerySemolina
A
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>
Anion answered 27/6, 2018 at 3:48 Comment(3)
I'm not sure if it will work with es modulesUlick
@Ulick - I am sure it does.Anion
it may, but I meant setAttribute. attributes are like strings afaik. so it basically means you write <button onclick="myfun()"></button> and if myfun is a function in a module, you can't reach it unless you make it global. piece of advice, don't make your surety become cheaper.Ulick
Q
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>
Quadragesima answered 2/5, 2019 at 20:41 Comment(0)
S
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
}
Steno answered 27/2, 2009 at 17:48 Comment(5)
I don't consider this a bug. Replacing an element means you completely replace it. It should not inherit what was there before.Hwang
But I don't want to replace an element; I just want to append new ones to the parent.Eristic
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.Steno
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”.Thiazine
You don't need to re-attach mydiv.onclick. Only the inner span's onclick is overridden by innerHTML +=.Peterson
M
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;
}
Maloriemalory answered 24/10, 2018 at 8:7 Comment(0)
E
0

Element#append can be used to insert elements at the end of another element. To insert HTML from a string, Range#createContextualFragment can be used.

const mydiv = document.getElementById("mydiv");
mydiv.append(document.createRange().createContextualFragment(`
    <div>Some HTML here</div>
`));

Or to just append text:

mydiv.append("some text"); // no need for document.createTextNode

If you don't need to insert HTML directly, consider using DOM methods to create the structure instead:

mydiv.append(
    Object.assign(document.createElement('div'), 
              { textContent: 'some text', className: 'my-class' }),
    document.createElement('hr'),
    "some more text"
); 
// can append multiple elements (mixed with strings which are converted to text nodes)

On a related note, the corresponding Element#prepend method can be used to insert elements at the start (before the first child) of an element.

Epistemic answered 9/3 at 3:55 Comment(0)
S
-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;
    }
}
Streusel answered 27/3, 2014 at 19:14 Comment(1)
How is this supposed to avoid losing event listeners?Ocko
L
-1

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

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

Limitation answered 12/7, 2018 at 18:45 Comment(3)
It destroys all events.Briefing
how is it really a solution? it destroys all the events!!Chinachinaberry
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.Ulick
W
-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>
Weaponry answered 21/7, 2019 at 9:8 Comment(0)
S
-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";
}
Sabina answered 9/7, 2010 at 14:46 Comment(2)
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 shorterKondon
It's hilarious that in this completely wrong answer that cites avoiding extra typing as a justification, about half the code is redundant.Strum

© 2022 - 2024 — McMap. All rights reserved.