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.