Skip to content

Sorting ​

The sort model is a flat array: SortControl[]. Multi-sort is the default — later entries break ties of earlier ones. The state contract is the same as for filters: pure mutators, single watcher reaction.

The sort model ​

typescript
interface SortControl {
  field: string; // column path (dot-notation)
  direction: "asc" | "desc";
}

State lives at state.sorters. The renderer translates it into the $sort portion of Uniquery:

typescript
// state.sorters
[
  { field: "status", direction: "asc" },
  { field: "createdAt", direction: "desc" },
]

// → controls.$sort
{ status: 1, createdAt: -1 }

Order matters. The server applies the directives in the same order, so status is the primary sort and createdAt is the tiebreaker.

How sorters get into the state ​

Header click ​

Clicking a <AsTableHeaderCell> toggles the column's position in state.sorters:

  1. Not in sorters → appended as asc.
  2. Already asc → flipped to desc.
  3. Already desc → removed from the array.

Shift-click (or repeated clicks while another column is already sorted) preserves the rest of the array — that's how the user builds a multi-sort.

The header cell only fires the toggle when column.sortable === true, which itself derives from meta.fields[path].sortable (the server flag from @db.index.* / @db.column.sortable / @db.table.sortable). For non-sortable columns the header is inert.

The config dialog ​

<AsConfigDialog>'s Sorters tab is the heavy-duty editor:

  • Reorder sorters by drag.
  • Flip direction.
  • Remove sorters.
  • (Re-)add any sortable column.

All operations write back to state.sorters; nothing else changes until the watcher re-fires the query.

v-model:sorters ​

<AsTableRoot> accepts an external Ref<SortControl[]> via v-model:sorters. Useful for surfacing sort state in a parent component (URL state, persistence, sync between sibling tables).

Force sorters ​

<AsTableRoot :force-sorters="..."> (or the underlying useTable({ forceSorters })) pins a list of sorters that always prepend the user's:

vue
<AsTableRoot
  url="/db/tables/items"
  :force-sorters="[{ field: 'priority', direction: 'desc' }]"
>

The merge rule is implemented by mergeSorters in @atscript/ui-table:

typescript
import { mergeSorters } from "@atscript/ui-table";

mergeSorters(
  [{ field: "priority", direction: "desc" }],
  [
    { field: "priority", direction: "asc" },
    { field: "name", direction: "asc" },
  ],
);
// → [{ field: "priority", direction: "desc" }, { field: "name", direction: "asc" }]

If a user sorter targets the same field as a force sorter, the user entry is dropped from the merged result — the force entry wins. The user's array isn't mutated; only the merged result that flows into buildTableQuery is affected.

This is the natural reading of "force": the server always sees the force sorter, and the user can layer additional tie-breakers underneath but cannot override.

Programmatic sorting ​

For one-off application from the parent component, write directly to the model:

vue
<AsTableRoot ref="root" url="/db/tables/orders" v-slot="{ sorters }">
  <button
    @click="root!.state.sorters.value = [{ field: 'amount', direction: 'desc' }]"
  >
    Sort by amount
  </button>
  <AsTable />
</AsTableRoot>

Watchers downstream will re-fire the query automatically — you don't need to call state.query().

What ends up on the wire ​

buildTableQuery (in @atscript/ui-table) assembles the $sort controls block from the merged sorters:

typescript
const sorters = forceSorters?.length ? mergeSorters(forceSorters, userSorters) : userSorters;

const $sort: Record<string, 1 | -1> = {};
for (const s of sorters) {
  $sort[s.field] = s.direction === "asc" ? 1 : -1;
}

Backends consume the same shape — see atscript-db query syntax for the adapter-specific lowering.

Next steps ​

Released under the MIT License.