Table
A data table with search, column sorting, and pagination — all derived from raw
rows plus a few control fields in state. The visible page is one method
chaining filter → sort → slice; nothing is precomputed or cached.
Markup
<input data-model="query" @input="resetPage" placeholder="Search people..." />
<table>
<thead>
<tr>
<th @click="sortBy" data-bind="data-key:'name'">Name<span data-text="ind('name')"></span></th>
<th @click="sortBy" data-bind="data-key:'score'">Score<span data-text="ind('score')"></span></th>
</tr>
</thead>
<tbody>
<template data-each="visible()" data-key="id">
<tr>
<td data-text="item.name"></td>
<td data-text="item.score"></td>
</tr>
</template>
</tbody>
</table>
<span data-text="range()"></span>
<button @click="prev" data-bind="disabled:page === 0">Prev</button>
<button @click="next" data-bind="disabled:page >= pageCount() - 1">Next</button>
Component
Micra.define("table", {
state: {
query: "",
sortKey: "name",
sortDir: "asc",
page: 0,
pageSize: 5,
rows: [
{ id: 1, name: "Ada Lovelace", role: "Admin", score: 142 },
{ id: 2, name: "Bob Smith", role: "Member", score: 88 },
{ id: 3, name: "Carol White", role: "Viewer", score: 17 },
// ...
],
},
filtered() {
const q = this.state.query.trim().toLowerCase();
return this.state.rows.filter((r) => !q || r.name.toLowerCase().includes(q) || r.role.toLowerCase().includes(q));
},
sorted() {
const { sortKey, sortDir } = this.state;
const mult = sortDir === "asc" ? 1 : -1;
return [...this.filtered()].sort((a, b) => (a[sortKey] > b[sortKey] ? 1 : a[sortKey] < b[sortKey] ? -1 : 0) * mult);
},
visible() {
const { page, pageSize } = this.state;
return this.sorted().slice(page * pageSize, (page + 1) * pageSize);
},
total() {
return this.filtered().length;
},
pageCount() {
return Math.max(1, Math.ceil(this.total() / this.state.pageSize));
},
range() {
const t = this.total();
const start = t === 0 ? 0 : this.state.page * this.state.pageSize + 1;
return `${start}-${Math.min(start + this.state.pageSize - 1, t)} of ${t}`;
},
ind(key) {
if (this.state.sortKey !== key) return "";
return this.state.sortDir === "asc" ? " ^" : " v";
},
sortBy(e) {
const key = e.currentTarget.dataset.key;
if (this.state.sortKey === key) {
this.state.sortDir = this.state.sortDir === "asc" ? "desc" : "asc";
} else {
this.state.sortKey = key;
this.state.sortDir = "asc";
}
this.state.page = 0;
},
resetPage() {
this.state.page = 0;
},
prev() {
if (this.state.page > 0) this.state.page--;
},
next() {
if (this.state.page < this.pageCount() - 1) this.state.page++;
},
});
filtered, sorted, and visible compose as plain method calls. Because they
recompute on read, a single write to query, sortKey, or page re-renders the
table correctly with no derived state to invalidate.