The element you were trying to find wasn’t in the DOM when your script ran.
The position of your DOM-reliant script can have a profound effect on its behavior. Browsers parse HTML documents from top to bottom. Elements are added to the DOM and scripts are (by default) executed as they're encountered. This means that order matters. Typically, scripts can't find elements that appear later in the markup because those elements have yet to be added to the DOM.
Consider the following markup; script #1 fails to find the <div>
while script #2 succeeds:
<script>
console.log("script #1:", document.getElementById("test")); // null
</script>
<div id="test">test div</div>
<script>
console.log("script #2:", document.getElementById("test")); // <div id="test" ...
</script>
So, what should you do? You've got a few options:
Option 1: Move your script
Given what we've seen in the example above, an intuitive solution might be to simply move your script down the markup, past the elements you'd like to access. In fact, for a long time, placing scripts at the bottom of the page was considered a best practice for a variety of reasons. Organized in this fashion, the rest of the document would be parsed before executing your script:
<body>
<button id="test">click me</button>
<script>
document.getElementById("test").addEventListener("click", function() {
console.log("clicked:", this);
});
</script>
</body><!-- closing body tag -->
While this makes sense, and is a solid option for legacy browsers, it's limited and there are more flexible, modern approaches available.
Option 2: The defer
attribute
While we did say that scripts are, "(by default) executed as they're encountered," modern browsers allow you to specify a different behavior. If you're linking an external script, you can make use of the defer
attribute.
[defer
, a Boolean attribute,] is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded
.
This means that you can place a script tagged with defer
anywhere, even the <head>
, and it should have access to the fully realized DOM.
<script src="https://gh-canon.github.io/misc-demos/log-test-click.js" defer></script>
<button id="test">click me</button>
Just keep in mind...
defer
can only be used for external scripts, i.e.: those having a src
attribute.
- be aware of browser support, i.e.: buggy implementation in IE < 10
Option 3: Modules
Depending upon your requirements, you may be able to utilize JavaScript modules. Among other important distinctions from standard scripts (noted here), modules are deferred automatically and are not limited to external sources.
Set your script's type
to module
, e.g.:
<script type="module">
document.getElementById("test").addEventListener("click", function(e) {
console.log("clicked: ", this);
});
</script>
<button id="test">click me</button>
Option 4: Defer with event handling
Add a listener to an event that fires after your document has been parsed.
DOMContentLoaded event
DOMContentLoaded
fires after the DOM has been completely constructed from the initial parse, without waiting for things like stylesheets or images to load.
<script>
document.addEventListener("DOMContentLoaded", function(e){
document.getElementById("test").addEventListener("click", function(e) {
console.log("clicked:", this);
});
});
</script>
<button id="test">click me</button>
Window: load event
The load
event fires after DOMContentLoaded
and additional resources like stylesheets and images have been loaded. For that reason, it fires later than desired for our purposes. Still, if you're considering older browsers like IE8, the support is nearly universal. Granted, you may want a polyfill for addEventListener()
.
<script>
window.addEventListener("load", function(e){
document.getElementById("test").addEventListener("click", function(e) {
console.log("clicked:", this);
});
});
</script>
<button id="test">click me</button>
jQuery's ready()
DOMContentLoaded
and window:load
each have their caveats. jQuery's ready()
delivers a hybrid solution, using DOMContentLoaded
when possible, failing over to window:load
when necessary, and firing its callback immediately if the DOM is already complete.
You can pass your ready handler directly to jQuery as $(handler)
, e.g.:
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<script>
$(function() {
$("#test").click(function() {
console.log("clicked:", this);
});
});
</script>
<button id="test">click me</button>
Option 5: Event Delegation
Delegate the event handling to an ancestor of the target element.
When an element raises an event (provided that it's a bubbling event and nothing stops its propagation), each parent in that element's ancestry, all the way up to window
, receives the event as well. That allows us to attach a handler to an existing element and sample events as they bubble up from its descendants... even from descendants added after the handler was attached. All we have to do is check the event to see whether it was raised by the desired element and, if so, run our code.
Typically, this pattern is reserved for elements that don't exist at load time or to avoid attaching a large number of duplicate handlers. For efficiency, select the nearest reliable ancestor of the target element rather than attaching it to the document
.
Native JavaScript
<div id="ancestor"><!-- nearest ancestor available to our script -->
<script>
document.getElementById("ancestor").addEventListener("click", function(e) {
if (e.target.id === "descendant") {
console.log("clicked:", e.target);
}
});
</script>
<button id="descendant">click me</button>
</div>
jQuery's on()
jQuery makes this functionality available through on()
. Given an event name, a selector for the desired descendant, and an event handler, it will resolve your delegated event handling and manage your this
context:
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<div id="ancestor"><!-- nearest ancestor available to our script -->
<script>
$("#ancestor").on("click", "#descendant", function(e) {
console.log("clicked:", this);
});
</script>
<button id="descendant">click me</button>
</div>