The problem
In the events lesson you saw that .on() only attaches handlers to elements that exist when it runs. In real pages, elements come and go all the time: rows added from Ajax, to-do items created by the user, search results replaced on every search. Attaching handlers to each new element is tedious and easy to forget.
The solution: listen on the container
Events bubble: a click on a list item is also a click on the list, on the body, and on the document. So you can attach one handler to a container that always exists, and let it check where each click came from. jQuery does the checking for you when you pass a selector as the second argument:
<ul id="list"><li>First item</li></ul>
<button id="add">Add item</button>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
let n = 1;
// Delegated: attached to #list, runs for any li inside, now or later
$("#list").on("click", "li", function () {
console.log("clicked:", $(this).text());
});
$("#add").on("click", function () {
n++;
$("#list").append("<li>Item " + n + "</li>");
});
</script>clicked: Item 3Two items were added after the handler was attached, and clicking the newest one still worked. Compare this with the example in the events lesson, where the new item did nothing.
How it works
When a click reaches #list, jQuery looks at event.target and walks up from it to #list, checking each element against the selector "li". For every match, it calls your handler with this set to the matching element. So:
this(andevent.currentTarget) is the matchedli, not the list;event.delegateTargetis the element the handler is attached to,#list;event.targetis still the innermost element clicked, which may be inside theli.
<table border="1" cellpadding="4">
<tbody id="rows">
<tr data-id="11"><td>Lamp</td><td><button class="del">Delete</button></td></tr>
<tr data-id="12"><td>Desk</td><td><button class="del">Delete</button></td></tr>
</tbody>
</table>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#rows").on("click", ".del", function (event) {
const $row = $(this).closest("tr");
console.log("this:", this.className, "| delegateTarget:", event.delegateTarget.id, "| row id:", $row.data("id"));
$row.remove();
console.log("rows left:", $("#rows tr").length);
});
</script>this: del | delegateTarget: rows | row id: 12
rows left: 1Why it is better
Delegation has three advantages:
- New elements just work. No need to attach handlers after every insert.
- One handler instead of many. A table with 1,000 rows needs one handler, not 1,000, which saves memory and setup time.
- Removing elements is clean. Removed rows had no handlers of their own, so nothing needs tidying.
That is why delegation is the normal way to handle events in lists, tables, menus and anything built from data.
A complete example
A small task list, using delegation for both actions on each item:
<style> .is-done span { text-decoration: line-through; color: gray; } li button { margin-left: 6px; } </style>
<form id="new"><input id="task" placeholder="New task"> <button>Add</button></form>
<ul id="tasks">
<li><span>Pay rent</span><button class="done">Done</button><button class="remove">Remove</button></li>
</ul>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#new").on("submit", function (event) {
event.preventDefault();
const text = $("#task").val().trim();
if (!text) return;
$("<li>").append(
$("<span>", { text }),
$("<button>", { "class": "done", text: "Done" }),
$("<button>", { "class": "remove", text: "Remove" })
).appendTo("#tasks");
$("#task").val("");
});
$("#tasks")
.on("click", ".done", function () {
$(this).closest("li").toggleClass("is-done");
})
.on("click", ".remove", function () {
$(this).closest("li").remove();
});
$("#tasks").on("click", "button", function () {
console.log($("#tasks li").map((i, el) => $(el).find("span").text() + ($(el).hasClass("is-done") ? " (done)" : "")).get().join(", "));
});
</script>Pay rent (done), Water plants
Pay rent (done)The new task, added after the handlers were attached, could still be removed. The project at the end of the course builds a fuller version of this.
Choosing the container
Attach the delegated handler to the closest ancestor that always exists:
- good:
$("#tasks").on("click", ".remove", ...); - works, but less good:
$(document).on("click", ".remove", ...).
Delegating to document is sometimes necessary (for example, when the container itself is replaced by Ajax), but then every click anywhere on the page has to be checked against your selector, and a stopPropagation() somewhere deeper can stop it from ever arriving.
Events that do not bubble
Delegation depends on bubbling. Most events bubble, but a few do not: focus, blur, mouseenter, mouseleave, load and scroll (on elements). jQuery quietly handles the common cases for you: a delegated focus or blur handler is converted to focusin/focusout, and delegated mouseenter/mouseleave are emulated with mouseover/mouseout. For load and scroll, attach directly.
Old methods you may meet
Before .on() existed, delegation was written as .live() (removed in jQuery 1.9) or $(container).delegate(selector, event, fn) (deprecated; note the different argument order). Rewrite both as $(container).on(event, selector, fn).
Common mistakes
- Forgetting the selector argument, which makes a normal direct handler.
- Delegating to a container that is itself replaced, which throws the handler away.
- Using
thisexpecting the container; it is the matched child. - Delegating everything to
documentby habit.
Exercise
Build a list of three colours. Using one delegated handler on the list, clicking any item should log its text. Add a button that adds a fourth colour, and check that clicking it also works.
Show one possible answer
<ul id="colours"><li>Red</li><li>Green</li><li>Blue</li></ul>
<button id="more">Add purple</button>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$("#colours").on("click", "li", function () { console.log($(this).text()); });
$("#more").on("click", function () { $("#colours").append("<li>Purple</li>"); });
</script>PurpleKey points
- A delegated handler is attached to a container and runs for events that bubble up from children matching a selector.
- Write it as $(container).on("click", ".item", handler); inside, this is the matching child.
- It works for elements added after the handler was attached, and uses one handler instead of hundreds.
- Attach to the closest container that always exists, not to document, and only for events that bubble.