Here is a what you're trying to do in vanilla javascript:
var body = document.body;
var pages = {};
['home', 'other'].forEach(function(page){
var p = pages[page] = document.getElementById(page);
body.removeChild(p);
});
go = function(where){
body.innerHTML = '';
body.appendChild(pages[where]);
}
go('home');
<body>
<div id='home'>
<h1>Home Page</h1>
<button onclick="go('other')">Go to another Page</button>
</div>
<div id='other'>
<h1>Another Page</h1>
<button onclick="go('home')">Go Home</button>
</div>
</body>
I think you expected the call to jQuery method .html()
have the same effect of what the go()
method in above vanilla code is doing. Throwing out the old HTML, and putting in the fresh content - not actually HTML (=string), but rather a dom tree, with handlers already in it. That expectation is not at all unreasonable - but jQuery does not fulfill it, and I'm going to explain why that is.
The issue is NOT with the new stuff having handlers (jQuery is fine with that); the issue is with the OLD stuff being thrown out. .html()
, when throwing stuff out, doesn't just remove it from the Dom tree and forgets about it; it assumes (unlike .detach()
, see below) that that old stuff isn't something you're going to re-insert later, but trash, which must be 'incinerated' in order not to cause memory leaks.
From the docs:
When .html() is used to set an element's content, any content that was
in that element is completely replaced by the new content.
Additionally, jQuery removes other constructs such as data and event
handlers from child elements before replacing those elements with the
new content.
The way in which old dom nodes can cause memory leaks is this: Data associated with the dom nodes via the jQuery method .data()
is not stored in the node itself, but in a central jQuery-interal 'cache' - the node just has a reference into the cache, so the node can find its data. If you delete the node the vanilla way (parent.removeChild()
), the data of the old trash node will stay in the jQuery data cache and waste space there. I'm not sure how event handlers have similar issues as the above, but I'm sure their removal has a similar reason. That memory leak issue is discussed further here and in the upvoted comment to this question.
Now, in your code, the event handlers aren't set as attributes (onclick
), but with .on
, which is translated to the vanilla addEventListener
. And those (see quote from jQuery docs) are incinerated when .html()
removes the old content.
It is true that coding would be a little easier in some cases, if jQuery omitted above incineration, but then, the resulting memory leak issues would be way worse (because they are way harder to find compared to your little issue here), so the bad would outweigh the good, if jQuery were to live up to your expectations. (side note: if my memory doesn't fail me, there are old jQuery versions which behave that way, but don't quote me on that.)
Workarounds are manifold. The version closest to your original code (and above vanilla version, but using jQuery) is to circumvent the incineration, by using .detach()
which is the jQuery way of removing something from the dom tree without incinerating it. Then, indeed, 'event handlers follow the fragment around'.
Something like this:
var pages = {}, showing = null;
['home', 'other'].forEach(function(page){
pages[page] = $('#' + page).detach();
});
go = function(where){
if (showing){ pages[showing].detach(); }
$('body').append(pages[where]);
showing = where;
}
go('home')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<body>
<div id='home'>
<h1>Home Page</h1>
<button onclick="go('other')">Go to another Page</button>
</div>
<div id='other'>
<h1>Another Page</h1>
<button onclick="go('home')">Go Home</button>
</div>
</body>
I'm not sure if I agree, but most people don't use .detach()
, but instead let the incineration happen, and then recreate the handlers, like this (and, here, event handlers do not 'follow around'):
let homeHTML = `
<div>
<h1>Home page</h1>
<button class="btn btn-primary">Go to another page</button>
</div>`;
let anotherHTML = `
<div>
<h1>Another page</h1>
<button class="btn btn-primary">Go back</button>
</div>`;
function setHome(){
$("body").html(homeHTML);
$("body").find('.btn').on('click', setAnother);
}
function setAnother(){
$("body").html(anotherHTML);
$("body").find('.btn').on('click', setHome);
}
setHome();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
The jQuery team made the right decision with above incineration. But it does have a downside, and your question puts the finger on it.
Both above workarounds have something unsatisfactory (or 'ugly', if you're as extreme a perfectionist as I am). detach
-append
are two steps, and .html
-.on
are also two steps, but the whole thing should be one step. Content change in a single page app should be a one-liner akin to variable assignment. Such as (in pseudo code) library.set(body, 'otherPage')
or body <- otherPage
or so. The jQuery .html()
command isn't that one-liner. That it isn't, is not a bug, but not a beauty contest winning achievement either.