Appending HTML into the body causes a mobile "burger" menu not to work

I have a MediaWiki website and I load the following JavaScript code in all its webpages via the standard MediaWiki file for custom JavaScript:

newHTML = document.querySelector("body");
newHTML.innerHTML +=`
<!-- -->
`;

Loading this code makes the mobile burger menu of the website not to work:
Clicking on the menu won’t initiate a modal with menu links and there is no error in the browser console regarding this problem.

How would you suggest to handle that situation?

Hi @bendqh1, changing the innerHTML will create new DOM elements, so event listeners added before will stop working since those elements simply don’t exis on the page any more. To fix this, you can either

  1. Add the required event listeners anew after populating the new content, or
  2. Use event delegation which will also work with dynamic content

Are you sure you need to swap the entire document.body.innerHTML (including the navigation) anyway though? – Probably just the actual content would do as well here…

Hello @m3g4p0p

I don’t want to swap the entire document.body.innerHTML :slight_smile:
I do so just because I don’t want to touch the PHP templates of the content management system, so I try to append the HTML with JavaScript.

Maybe there is a better way to append it with JavaScript?

Well I don’t know your templates, but assuming you have some markup like this…

<!DOCTYPE html>
<html lang="en">
<head>
  <title>My Page</title>
</head>
<body>
  <nav>
    <!-- Navigation -->
  </nav>
  <main>
    <!-- Main content -->
  </main>
</body>
</html>

… you can keep the navigation by just setting the innerHTML of main:

const main = document.querySelector('main')
main.innerHTML = `
  <!-- New content -->
`

Edit: Ah sorry, I missed that you just want to append content… in that case, you might insertAdjacentHTML(), which will keep the existing elements before and after untouched; so previously added event listeners will keep working too:


document.body.insertAdjacentHTML('beforeend', `
  <!-- New content -->
`)

Thanks a lot. Indeed, the following pattern was exactly the one I needed; I have tested it with three browsers in mobile mode, two in smartphone (always after fully clearing all cache), and I no longer have the problem described in this post.

document.body.insertAdjacentHTML('beforeend', `
  <!-- New content -->
`)

I admit that it is still quite unclear to me what damage innerHTML has done, I mean, why it damaged event listeners that were defined outside the HTML anyway.

The browser doesn’t check if the HTML (or parts of it) are equal to the HTML as it were before, and
doesn’t reuse existing elements; so by setting the innerHTML (be it a regular assignment or an addition assignment) you’re always throwing away the existing content, and parsing the HTML string to new DOM elements.

1 Like

Thank you again !
All I can say is darn, I didn’t figure out it yet… :expressionless:
I still misunderstand why using innerHTML on the body tag prevents event listeners from doing their work (removes them completely?).

I tried to read in some posts about this but didn’t understand the “why” rationale.

Perhaps the very term “innerHTML” is misleading because it’s not merely “editing the inner HTML of a tag”, but rather “editing the inner HTML of a tag and also destroying any event listener around”.

Well not quite, actually it just replaces the element’s children with new children from parsing the HTML string… consider this:

document.body.innerHTML = `
  <button id="my-button">Click me</button>
`

const button = document.getElementById('my-button')

button.addEventListener('click', () => {
  console.log('clicked', button.id)
})

button.click() // Logs "clicked my-button"
console.log(button.parentElement) // Logs "<body></body>"

// Now set the inner HTML again
document.body.innerHTML = `
  <button id="my-button">Click me</button>
`

button.click() // Still logs "clicked my-button"
console.log(button.parentElement) // Now logs "null"

As you can see, the original button element still exists – as does the event listener – it just isn’t attached to the body any more. Even though the new markup happens to be the same as the original one, setting the innerHTML always creates a whole new element tree… and normally the old detached children will get garbage collected and effectively be destroyed.

And if we now click the “new” button, nothing will happen as it doesn’t have any event listeners added yet:

const newButton = document.getElementById('my-button')

newButton.click() // Logs nothing
console.log(button === newButton) // Logs "false"

My logging in Google Chrome 94.0.4606.61 (official build) (64-bit) in Windows 10 home is different; here is the full trace:

document.body.innerHTML = `
  <button id="my-button">Click me</button>
`

‘\n Click me\n’

const button = document.getElementById('my-button')

undefined

button.addEventListener('click', () => {
  console.log('clicked', button.id)
})

undefined

button.click() // Logs "clicked my-button"

undefined

console.log(button.parentElement) // Logs "<body></body>"

undefined

// Now set the inner HTML again
document.body.innerHTML = `
  <button id="my-button">Click me</button>
`

‘\n Click me\n’

button.click() // Still logs "clicked my-button"

undefined

console.log(button.parentElement) // Now logs "null"

undefined

const newButton = document.getElementById('my-button')

undefined

newButton.click() // Logs nothing

undefined

console.log(button === newButton) // Logs "false"

undefined


If the innerHTML property makes the worked-on DOM tree content to be copy-pasted into a new DOM tree and the new DOM tree lacks connection to the JavaScript of the previous DOM tree (I still miss why even do such a thing) than the naming of the property was misleading and instead innerHTML it probably should have been something like innerHTMLOfACopyPastedCurrentDomIntoANewOneStrippedOfAnyJS.
Am I missing something?

Where did you execute that code? Have a look at this code pen, I’m getting:

"clicked" "my-button"

<body>
  <button id="my-button">Click me</button>
</body>

"clicked" "my-button"

null

false

Exactly, it’s an entirely new DOM tree with its own event listeners (or lack thereof); if you need to keep the existing tree, use insertAdjacentHTML() instead.

You typically don’t; only set the innerHTML if you want to 1) initially populate an element, or 2) deliberately wipe an existing tree and create a fresh one (and then you’d just clear the children like el.innerHTML = '').

@m3g4p0p thanks, I ran the code here on this very webpage (after CTRL+Shift+R).