DOMContentLoaded, defer and anonymous functions
Every script so far has lived at the bottom of <body>. That works, but it ties the script to its position in the file, and the day someone moves it into <head>, every querySelector comes back null. This lesson fixes the timing properly. It also introduces the short, nameless functions you will write in nearly every listener from here on.
What the browser does with a page
Here is the order of events, simplified, for a page with a stylesheet, an image and a script in the head:
- The browser starts reading the HTML at the top and creates elements as it goes.
- It reaches a plain
<script src="app.js">, pauses, downloads the file and runs it. At this moment<body>does not exist yet, so the script cannot find anything in it. - It carries on reading the rest of the HTML.
- Once the whole document has been read, it runs any
deferscripts in the order they appear, then fires theDOMContentLoadedevent ondocument. - When images and other files have finished loading as well, it fires
loadonwindow.
Step 2 is the problem from the first lesson. Step 4 offers two ways out.
Option one: wait for DOMContentLoaded
<head>
<meta charset="utf-8">
<title>Bench notes</title>
<script>
document.addEventListener('DOMContentLoaded', function () {
const button = document.querySelector('#toggle-notes');
button.addEventListener('click', function () {
document.body.classList.toggle('notes-hidden');
});
});
</script>
</head>
The outer function runs only after the document has been read, so every element in the HTML exists by the time its first line runs. DOMContentLoaded does not wait for images, which makes it fire earlier than load. Use load only when your code needs something that arrives later, such as the real size of a picture.
One trap: if a script adds this listener after the event has already fired, the function never runs and nothing reports a problem. That mostly affects code loaded late by other code. Such scripts check document.readyState first, which stays 'loading' until the document has been read.
Option two: the defer attribute
<head>
<meta charset="utf-8">
<title>Bench notes</title>
<link rel="stylesheet" href="style.css">
<script src="app.js" defer></script>
</head>
defer tells the browser to download the file in the background while it keeps reading the HTML, then run the script after the document has been read, just before DOMContentLoaded. Several deferred scripts run in the order they appear in the page. Inside app.js you can call querySelector on the first line with no wrapper at all.
Two details are easy to miss. defer only applies to external scripts, the ones with a src; on an inline <script> it has no effect. And a script with type="module" is deferred automatically, attribute or not.
For files you write yourself, defer in the head is the tidy default, and the rest of this course uses it. DOMContentLoaded remains the tool for inline scripts. You will also see async, which runs a script as soon as it has downloaded, in no guaranteed order. That suits a standalone script that never touches your elements, not your page logic.
Anonymous functions
Look at option one again. Neither function has a name. Each is written exactly where it is needed and passed straight to addEventListener. That is an anonymous function, and it is the usual way to write a listener used in one place only.
The same click handler, named and anonymous:
// Named: defined once, passed by name
function hideNotes() {
document.body.classList.add('notes-hidden');
}
button.addEventListener('click', hideNotes);
// Anonymous: defined in place
button.addEventListener('click', function () {
document.body.classList.add('notes-hidden');
});
Both behave the same. The named version can be reused and removed later with removeEventListener, as the events lesson showed. The anonymous version keeps the code right next to the element it affects, which is easier to read when the function is three lines long.
Arrow functions
Modern JavaScript has a shorter way to write an anonymous function:
button.addEventListener('click', () => {
document.body.classList.add('notes-hidden');
});
field.addEventListener('input', (event) => {
preview.textContent = event.target.value;
});
The => arrow takes the place of the word function, and the parameters sit in the parentheses before it. With exactly one parameter, the parentheses are optional, so event => { ... } is also valid. When the body is a single expression, the braces can go too; the next lesson shows that form.
The difference that matters in event code is this. Inside a listener written with function, this is the element the listener is attached to. An arrow function has no this of its own and uses whatever this means in the surrounding code, which at the top level of a script is not your button. The simple way out is to skip this entirely and use event.currentTarget, which gives you the element whichever style you write.
Try it
In the online HTML editor, put the bench-notes markup in the body, and the .notes-hidden style rule from the events lesson plus the DOMContentLoaded script from option one in the head. Click the button to check that it works. Now delete the outer document.addEventListener(...) wrapper, keep only what was inside it, and run the page again: the console shows the familiar null error. Put the wrapper back, then rewrite both functions as arrow functions and confirm the button still toggles the notes.