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.