Loops & Keyed Lists
Lists render through <template pp-for> — the directive lives only on a <template> element, and every repeated row carries a key.
Basics
<ul>
<template pp-for="user in users">
<li key="{user.id}">{user.name}</li>
</template>
</ul>
With the index:
<ol>
<template pp-for="(step, index) in steps">
<li key="{step.id}">{index + 1}. {step.label}</li>
</template>
</ol>
Keys
- Use a stable identity from your data (
user.id), never the array index — index keys break reordering and removal. - The
keygoes on the repeated element itself, inside the template. - One root element per row. A row that needs siblings wraps them in one parent.
Tables and selects
Because <template> is legal anywhere, keyed rows work inside
<tbody>, <tr> and <select> without the
foster-parenting problems wrapper elements would cause:
<table>
<tbody>
<template pp-for="row in rows">
<tr key="{row.id}">
<td>{row.name}</td>
<td>{row.total}</td>
<td><button onclick="remove(row.id)">Delete</button></td>
</tr>
</template>
</tbody>
</table>
Not JSX
<!-- WRONG — JSX. Renders one literal row, or nothing:
{users.map(user => (<li>{user.name}</li>))}
-->
<!-- RIGHT -->
<template pp-for="user in users">
<li key="{user.id}">{user.name}</li>
</template>
Per-row reconciliation (v2)
PulsePoint v2 remembers the markup each keyed row produced. On re-render, a row whose output is byte-identical is reused — no re-parse, no attribute sync, no event rebinding for that subtree — and three or more consecutive reused rows collapse into a single run marker, so the cost of a mostly-unchanged list stops scaling with its length. Recognition is positional first (one string comparison for the common case) with a key lookup fallback, so a row that moved rather than changed is still recognized.
What this means for authors:
- Keep keys stable and rows deterministic — a row that renders the same bytes for the same data is a row the runtime never touches again.
- Avoid embedding always-changing values (timestamps, random ids) in row markup; they defeat reuse.
- Nested loops and multi-root row bodies opt out of the cache automatically — the list still renders correctly, just without the fast path.