Table
Charming::Components::Table renders tabular data with unicode borders (via tty-table), a highlighted selected row, keyboard navigation, sorting, and mouse click selection. It is interactive — give it a focus slot when the user should navigate rows. Reach for it when data has columns; for a flat pick-one list use List.
┌─────────┬───────┐
│Name │Stars ▼│
│bubbletea│5400 │
│charm │1200 │
│glow │800 │
└─────────┴───────┘
The separator line under the header is trimmed for a compact layout, and the sorted column is marked ▲/▼.
Quick start
class ReposController < ApplicationController
focus_ring :repos
def show
repos_state.selected_index = repos.selected_index
render :show, repos_table: repos
end
# Enter opens the selected row.
def repos_selected(row)
navigate_to "/repos/#{row.first}"
end
def repos
@repos ||= Charming::Components::Table.new(
header: ["Name", "Stars"],
rows: Repo.all.map { |repo| [repo.name, repo.stars] },
selected_index: repos_state.selected_index,
height: [screen.height - 10, 3].max,
theme: theme
)
end
private
def repos_state
state(:repos, ReposState)
end
end
<%= render_component repos_table %>
Options
| Option | Default | What it does |
|---|---|---|
header: | (required) | Array of column labels (scalars are wrapped and stringified). |
rows: | [] | Body rows — each a String, an Array, or a Hash of column-value pairs. Excess cells merge into the last column. |
selected_index: | 0 | The initially selected row, clamped into range. |
keymap: | :vim | Keybinding style — :vim adds j/k aliases; pass nil to disable. |
theme: | nil | Theme used for the selected-row style. |
height: | nil | Limits the visible body rows; the window auto-scrolls to keep the selection in view, and page keys move by a full window. |
Keyboard
| Key | Action |
|---|---|
up / k | Move selection up one row. |
down / j | Move selection down one row. |
home / end | Jump to first / last row. |
page_up / page_down | Move by one window of rows. |
enter | Select the highlighted row. |
j/k come from the default keymap: :vim. All keys return nil when the table has no rows.
Mouse
A click within the body area selects the clicked row. The handler subtracts HEADER_HEIGHT (2 rows: top border + header line) from the click’s y, then maps it relative to the visible window when height: is set.
What it returns
| Return value | Meaning |
|---|---|
[:selected, row] | Enter pressed — dispatches <slot>_selected(row) with the row as constructed (String, Array, or Hash). |
:handled | A navigation key or click was consumed. |
nil | The event was not handled. |
See the shared component protocol.
Working with it
selected_row— the highlighted row, ornilfor an empty table.sort_by!(column, direction: :asc)— sorts rows by a header label or 0-based index; numeric-looking cells compare numerically, everything else as strings. Returnsself.toggle_sort(column)— sorts bycolumn, flipping direction on repeated calls (ascending first). Returnsself.header/rows/selected_index— readers for the constructor state.
Tips
- Components are rebuilt on every event. Persist
selected_indexin controller state and pass it back in on construction (see the quick start), or the selection resets on every keypress. - Sorting mutates the instance’s rows — a rebuilt table is unsorted again. Store the sort column and direction in state and reapply
sort_by!after constructing:table.sort_by!(state.sort_column, direction: state.sort_direction). toggle_sortonly flips when called twice on the same instance for the same column; across rebuilds, track the direction yourself and usesort_by!.- With header and rows both empty,
renderreturns the placeholder string(empty table).