Progressive interface foundations
Connect behavior with data attributes
Use data attributes and one delegated event handler to connect repeated task controls to small native behaviors.
A project board repeats the same actions on many cards. You do not need one click listener per button. Put app-specific metadata in data-* attributes and let one listener on the stable board find the control that fired.
Event delegation works because clicks bubble. closest() catches clicks on an icon inside the button. contains() confirms the button belongs to this board. New cards work without another init pass.
<section id="board">
<article data-task-id="task-17">
<h2>Write homepage</h2>
<button type="button" data-action="complete">Complete</button>
</article>
</section>
<script>
const board = document.querySelector('#board')
board.addEventListener('click', event => {
const button = event.target.closest('button[data-action]')
if (!button || !board.contains(button)) return
const card = button.closest('[data-task-id]')
if (button.dataset.action === 'complete') card.toggleAttribute('data-complete')
})
</script>
Click Complete and inspect the card in DevTools. The data-complete attribute appears or disappears. One listener handles every card.
Use data attributes for local IDs, modes, and hooks that have no standard HTML attribute. Do not store a whole app state tree in strings. The task title stays text. Completion can be a present or absent attribute. Richer records can live in normal JavaScript data.
A delegated handler should still respond to real buttons and links. Do not make the entire card clickable when it also contains nested controls. Accidental activation gets easy fast.
Validate action names before you use them. A switch or explicit handler map beats building a function name from untrusted attribute text. The markup is a contract between HTML and JavaScript, so keep it small. I treat unknown data-action values as no-ops and log them in development. That catches typos in markup before they reach production users.
Try this on your board: duplicate the article three times with unique task IDs, add a fourth card after load, and complete every card with the same listener. Use the keyboard too. If clicking an icon inside the button does nothing, replace direct event.target.matches() checks with closest() as shown.
Lesson completed