Core · jQuery-compatible

Event delegation

Bind once on a container, handle events for every matching descendant — present and future. One listener instead of thousands, and nothing to re-bind after you add content.

Why delegation wins

Binding a handler per row costs memory and setup time, and every row you add later needs binding again. Delegation attaches one listener to a stable ancestor. When an event bubbles up, jsKjs checks event.target.closest(selector) and calls your handler with this set to the matching element.

It also pairs perfectly with Hyper: content swapped in by kj-get is handled automatically, because the listener lives above it.

Live · 1,000 rows, one listener

0 rows · 1 listener
#OrderTotalActions

Click a row to select it.

View the source for this demo
HTML
<div class="row">
  <button type="button" id="dl-add">Add 250 rows</button>
  <button type="button" id="dl-reset">Reset</button>
  <span class="chip" id="dl-stats" aria-live="polite">0 rows · 1 listener</span>
</div>
<div class="dl-wrap" tabindex="0" role="region" aria-label="Orders table (scrollable)">
  <table>
    <thead><tr><th scope="col">#</th><th scope="col">Order</th><th scope="col">Total</th><th scope="col"><span class="sr-only">Actions</span></th></tr></thead>
    <tbody id="dl-body"></tbody>
  </table>
</div>
<p class="out" id="dl-out" aria-live="polite">Click a row to select it.</p>
JavaScript
let n = 0;
const stats = () => $('#dl-stats').text($('#dl-body tr').length + ' rows · 1 listener');

function addRows(count) {
  const rows = [];
  for (let i = 0; i < count; i++) {
    n++;
    rows.push('<tr data-id="' + n + '"><td>' + n + '</td><td>Order #' + (1000 + n) + '</td><td>$' + (n * 7 % 90 + 10) +
      '</td><td><button type="button" class="dl-del" aria-label="Delete order ' + n + '">✕</button></td></tr>');
  }
  $('#dl-body').append(rows.join(''));
  stats();
}

// The ONLY listener: it serves every row that exists now or later
$('#dl-body').on('click', 'tr', function (e) {
  if ($(e.target).closest('.dl-del').length) {
    const id = $(this).remove().attr('data-id');
    $('#dl-out').text('Deleted row ' + id + ' — nothing to unbind.');
    return stats();
  }
  $(this).toggleClass('sel');
  $('#dl-out').text('Row ' + this.dataset.id + ' toggled · ' + $('#dl-body tr.sel').length + ' selected');
});

$('#dl-add').on('click', () => addRows(250));
$('#dl-reset').on('click', () => { $('#dl-body').empty(); n = 0; addRows(20); $('#dl-out').text('Reset.'); });
addRows(20);

More examples

Delegate from the document for app-wide behaviour

JavaScript
$(document).on('click', '[data-copy]', function () {
  navigator.clipboard.writeText(this.dataset.copy);
});
$(document).on('submit', 'form[data-confirm]', function (e) {
  if (!confirm(this.dataset.confirm)) e.preventDefault();
});

API

.on(types [, delegateSelector], handler) → self

Attach handlers. Multiple space-separated events and .namespace suffixes are supported. With a delegate selector, the handler fires for matching descendants — including ones added later — and this is the matched element.

Reference: .on() →
.closest(selector) → KJ

Nearest matching ancestor (including the element itself). The backbone of event delegation.

Reference: .closest() →

Migrating

Coming from jQueryIdentical: .on(events, selector, handler). The old .live() and .delegate() forms are gone in favour of this one.

Related features