What you'll be able to do
By the end of this page you should be able to attach listeners with addEventListener, read the event object, call preventDefault when the default browser action gets in the way, and spot double-bound handlers.
Previous post: forms and user input.
Who this is for
- People who used
onclick="..."in HTML and want the JS-file style - Beginners whose button “fires twice” after a re-render
- Anyone submitting forms or links that should not reload the page
You can skip this if listeners feel routine. Come back when duplicate handlers or default link behavior bites you.
addEventListener basics
const button = document.querySelector("#save");
button.addEventListener("click", function (event) {
console.log("clicked", event.target);
});
Scenario A — separate HTML and behavior. Logic lives in <script> or a module.
Scenario B — multiple listeners on one element. addEventListener stacks them; old onclick = overwrites the previous one.
preventDefault
link.addEventListener("click", function (event) {
event.preventDefault();
// custom in-page navigation
});
Scenario A — <a href="#"> that should not jump.
Scenario B — form submit handled in JS (you saw this in the forms post).
Does not stop bubbling — that is the next posts.
Double-bound handlers
function onSave() { console.log("save"); }
button.addEventListener("click", onSave);
button.addEventListener("click", onSave); // fires twice per click
Scenario A — render function runs again and adds another listener without removing the old one.
Fix habits: bind once after DOM ready; or remove with removeEventListener (same function reference); or use delegation (post #47).
Named vs inline functions for removeEventListener
function handleClick() { /* ... */ }
btn.addEventListener("click", handleClick);
btn.removeEventListener("click", handleClick);
Anonymous functions cannot be removed unless you keep the reference — another reason named handlers help.
Common event types (starter set)
| Event | When |
|---|---|
click | buttons, links |
input | text fields as user types |
change | commit on select/checkbox |
submit | form submit |
Scenario A — live search box. input event.
Scenario B — dropdown picked. change event.
A tiny practice file
Save as events.html.
<!DOCTYPE html>
<html>
<body>
<a id="fake" href="https://example.com">Go (blocked)</a>
<button id="once">Log once per setup</button>
<pre id="out"></pre>
<script>
const out = document.getElementById("out");
document.getElementById("fake").addEventListener("click", function (e) {
e.preventDefault();
out.textContent = "Default navigation prevented.";
});
function logClick() {
out.textContent = (out.textContent || "") + "click\n";
}
const btn = document.getElementById("once");
btn.addEventListener("click", logClick);
// btn.addEventListener("click", logClick); // uncomment to see double fire
</script>
</body>
</html>
Mistakes I see a lot
1. Adding listeners inside a loop over the same parent without delegation.
2. Double add on every re-render.
3. Confusing preventDefault with stopPropagation.
4. Using click on disabled buttons — they do not fire; style alone is not enough for accessibility.
5. Forgetting { once: true } for one-shot setup when appropriate.
What to try before the next post
preventDefaulton a link.- Add the same listener twice; observe double logs.
- Remove a named listener.
- Build
events.html.
Next in this series: event bubbling and capturing — why a click on a button also reaches its parents.
Try this next outside the series
Browser JavaScript stops feeling isolated once you connect it to forms and component-oriented UI work.
- React forms without pain — see modern form handling after you understand the DOM version first
- HTML to JSX Converter — move tiny HTML snippets into React-style markup when you're ready