Try it: type, then reorder
Type a note into any row, then reverse the list or move the last row to the top. The note inputs have no binding at all - they keep what you typed because each row is the same element, moved rather than rebuilt.
The note inputs have no binding at all. They keep what you typed because each row is the same element, moved - not rebuilt.
<div class="demo-stage">
<div class="demo-controls">
<button class="btn btn-sm" data-on-click="reverse">Reverse</button>
<button class="btn btn-sm" data-on-click="rotate">Last to first</button>
<button class="btn btn-sm btn-primary" data-on-click="add">Add a row</button>
</div>
<ul class="demo-list rx-rows" data-each="rows key=id">
<li class="rx-row">
<span class="rx-row-name" data-bind-text="name"></span>
<input class="form-input" placeholder="Type a note here, then reorder">
<button class="btn btn-sm btn-ghost" data-on-click="$parent.remove($data)" aria-label="Remove">×</button>
</li>
</ul>
<p class="demo-note">The note inputs have no binding at all. They keep what you typed because each row is the same element, moved - not rebuilt.</p>
</div>/**
* Keyed lists keep their DOM.
*
* `data-each="rows key=id"` reconciles by key: a row that stays in the list
* keeps its actual element, so the note you typed into it - which no binding
* knows about - travels with it when the list is reordered.
*/
function identity() {
let next = 4;
const rows = observableArray([
{id: 1, name: 'Ada Lovelace'},
{id: 2, name: 'Grace Hopper'},
{id: 3, name: 'Katherine Johnson'}
]);
return {
rows,
reverse() { rows.reverse(); },
rotate() { rows.unshift(rows.pop()); },
add() { rows.push({id: next, name: `New person ${next++}`}); },
remove(row) { rows.remove(row); }
};
}Why the key matters
data-each="rows key=id" (or {{#each rows key=id}} in a template) reconciles by the key you name. An item that stays
in the collection keeps its DOM nodes and its effects, so focus, scroll position, CSS transitions and media playback
all survive a change to the list. A key must be an identity, not a value: one that changes when the item's contents
change defeats the whole mechanism.
data-each insists on a key. Without one, {{#each}} falls back to re-rendering the block wholesale and says so once.
A whole list, with add and remove
Nothing to do. Add something above.
0 of 0 left
/**
* The smallest list app anyone actually writes: add, tick off, delete, with a
* derived summary and an empty state.
*
* `done: observable(false)` is the line worth staring at. `observableArray`
* tracks the array - pushes, removes, reorders - not the fields inside its
* items. A plain `done: false` would tick in the DOM and change nothing else.
*/
function todo() {
const todos = observableArray([
{id: 1, title: 'Read the twelve bindings', done: observable(true)},
{id: 2, title: 'Bind a form with no change handler', done: observable(false)},
{id: 3, title: 'Delete a row and watch the others keep their DOM', done: observable(false)}
]);
const draft = observable('');
const composing = observable(false);
let nextId = 4;
return {
todos,
draft,
composing,
ready: computed(() => draft.value.trim() !== ''),
summary: computed(() => {
const all = todos.value;
const left = all.filter(t => !t.done.value).length;
return `${left} of ${all.length} left`;
}),
add() {
if (draft.value.trim() === '') return;
todos.push({id: nextId++, title: draft.value.trim(), done: observable(false)});
draft.value = '';
composing.value = true; // put the caret back in the field
},
// Inside a list `$data` is the item, so a row reaches the list that
// owns it through `$parent` - `data-on-click="$parent.remove($data)"`.
remove(item) { todos.remove(item); }
};
}Inside a row
Each item gets its own binding context: $data is the row, $parent the list's owner, $index its position, and
$parents / $parentContext reach further out. $parent.remove($data) is how a row's button reaches the list that
owns it - the one place an expression may call a method, because an event fires outside every effect.
Nest lists with {{#each}}, which reconciles at any depth. A nested data-each is inert and warns, naming the
mustache form to use instead.