Table
Sortable columns and pagination. Sort direction, sort key, and current page live in state — the rows shown (visible()) are derived. Switching sorts or pages is one state write; keyed data-each reuses DOM rows.
Live preview
| Name | Role | Signups | Joined |
|---|---|---|---|
Markup
<div data-component="table-demo">
<table class="data-table">
<thead>
<tr>
<th class="sortable" @click="sortBy" data-bind="data-key:'name'">
Name<span data-text="indicator('name')"></span>
</th>
<!-- … other headers -->
</tr>
</thead>
<tbody>
<template data-each="visible()" data-key="id">
<tr>
<td data-text="item.name"></td>
<td data-text="item.role"></td>
<td data-text="item.signups"></td>
<td data-text="item.joined"></td>
</tr>
</template>
</tbody>
</table>
<div class="table-pagination">
<span data-text="rangeLabel()"></span>
<button @click="prev" data-bind="disabled:page === 0">Prev</button>
<span data-text="(page + 1) + ' / ' + pageCount()"></span>
<button @click="next" data-bind="disabled:page >= pageCount() - 1">Next</button>
</div>
</div>
Definition
Micra.define('table-demo', {
state: {
rows: [
{ id: 1, name: 'Ada Lovelace', role: 'Admin', signups: 142, joined: '2024-01-12' },
{ id: 2, name: 'Bob Smith', role: 'Member', signups: 88, joined: '2024-03-04' },
{ id: 3, name: 'Carol White', role: 'Viewer', signups: 17, joined: '2024-06-21' },
{ id: 4, name: 'Diego Hernandez', role: 'Member', signups: 64, joined: '2024-07-08' },
{ id: 5, name: 'Esha Patel', role: 'Admin', signups: 203, joined: '2024-08-15' },
{ id: 6, name: 'Finn O’Brien',role: 'Member', signups: 31, joined: '2024-09-02' },
{ id: 7, name: 'Greta Lindberg', role: 'Viewer', signups: 9, joined: '2024-10-19' },
{ id: 8, name: 'Hiro Tanaka', role: 'Admin', signups: 116, joined: '2024-11-30' },
],
sortKey: 'name',
sortDir: 'asc',
page: 0,
pageSize: 4,
},
// ── derived values: ALWAYS methods ─────────────────────────
sorted() {
const { rows, sortKey, sortDir } = this.state
const mult = sortDir === 'asc' ? 1 : -1
return [...rows].sort((a, b) => {
const av = a[sortKey], bv = b[sortKey]
return (av > bv ? 1 : av < bv ? -1 : 0) * mult
})
},
visible() {
const { page, pageSize } = this.state
return this.sorted().slice(page * pageSize, (page + 1) * pageSize)
},
pageCount() {
return Math.max(1, Math.ceil(this.state.rows.length / this.state.pageSize))
},
rangeLabel() {
const start = this.state.page * this.state.pageSize + 1
const end = Math.min(start + this.state.pageSize - 1, this.state.rows.length)
return `Showing ${start}–${end} of ${this.state.rows.length}`
},
indicator(key) {
if (this.state.sortKey !== key) return ''
return this.state.sortDir === 'asc' ? ' ▲' : ' ▼'
},
// ── actions: mutate state, render is automatic ─────────────
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
},
prev() { if (this.state.page > 0) this.state.page-- },
next() { if (this.state.page < this.pageCount() - 1) this.state.page++ },
})
Integration
- Sorted + paginated rows are derived, not stored. Three pieces of input (rows, sortKey/sortDir, page) feed two methods (sorted(), visible()) — no manual "refresh" call ever.
- Keyed data-each on item.id means switching pages reuses <tr> DOM nodes — Micra just rewrites the cell text where it differs.
- Sort indicator is a method call (indicator(key)) — never stored. The header re-evaluates on each render, so the arrow tracks sortKey/sortDir without bookkeeping.
- Server-driven data works the same way — replace this.state.rows after a this.fetch() call. For truly large data sets, move sort + paginate server-side and only re-render visible()'s result.
Pitfalls
- Don't mutate this.state.rows with .sort() — it sorts in place, the proxy doesn't see it, and you've also destroyed the original order. Use [...rows].sort(…).
- Don't store visible, sorted, or pageCount in state. They're derived from primary state — a method per derived value keeps the source of truth single.
- Don't forget to reset page = 0 after a sort change — otherwise you can land on an empty page beyond the new end.