Skip to content

Sorting ​

Columns with sortable: true are sorted by clicking the header. Each click toggles the sort direction: ascending → descending → not sorted.

Hold Shift while clicking to sort by multiple columns. The number next to the sort icon shows the sort order.

The overview combines the most important options: an initially sorted column, a custom sort order, a column which sorts descending first and a column which is not sortable. The buttons set the sort model from outside of the grid.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="sort = [{ column: 'lastName', asc: true }, { column: 'firstName', asc: true }]">
        Sort by name
      </button>
      <button @click="sort = []">Clear sort</button>
      <code>{{ JSON.stringify(sort) }}</code>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      v-model:sortedColumns="sort"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = createEmployees(500);
const sort = ref<Array<{ column: string | number; asc: boolean }>>([]);

const roleOrder = ["Junior", "Senior", "Lead", "Principal"];

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  // initial sort direction
  { field: "department", text: "Department", sort: "asc" },
  {
    field: "role",
    text: "Role (custom order)",
    width: 170,
    // custom sort function
    sortFunc: (a, b) => roleOrder.indexOf(a.value) - roleOrder.indexOf(b.value),
  },
  {
    field: "salary",
    text: "Salary (desc first)",
    type: "number",
    width: 170,
    sortingOrder: ["desc", "asc", null],
  },
  { field: "rating", text: "Not sortable", type: "number", sortable: false },
];

const options: GridOptions<Employee> = {
  defaultColumn: { sortable: true, width: 150 },
};
</script>
tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions, type SortModel } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(500);

const roleOrder = ["Junior", "Senior", "Lead", "Principal"];

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  // initial sort direction
  { field: "department", text: "Department", sort: "asc" },
  {
    field: "role",
    text: "Role (custom order)",
    width: 170,
    // custom sort function
    sortFunc: (a, b) => roleOrder.indexOf(a.value) - roleOrder.indexOf(b.value),
  },
  {
    field: "salary",
    text: "Salary (desc first)",
    type: "number",
    width: 170,
    sortingOrder: ["desc", "asc", null],
  },
  { field: "rating", text: "Not sortable", type: "number", sortable: false },
];

const options: GridOptions<Employee> = {
  defaultColumn: { sortable: true, width: 150 },
};

export default function Sorting() {
  // undefined until the first change, so the initial sort of the columns is kept
  const [sort, setSort] = useState<SortModel>();

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button
          onClick={() =>
            setSort([
              { column: "lastName", asc: true },
              { column: "firstName", asc: true },
            ])
          }
        >
          Sort by name
        </button>
        <button onClick={() => setSort([])}>Clear sort</button>
        <code>{JSON.stringify(sort ?? [])}</code>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} sortedColumns={sort} onSortedColumnsChange={setSort} />
    </div>
  );
}

Multi column sorting ​

Sort by several columns when one column alone has many equal values, e.g. department first and salary within the department. Set sort on several columns for an initial multi column sort (the column order defines the sort order). Shift + click on another header adds it to the sort, Shift + click on a sorted column changes its direction or removes it from the sort. The option multiSortKey: "ctrl" uses Ctrl / ⌘ instead.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        Multi sort key
        <select v-model="multiSortKey">
          <option value="shift">Shift</option>
          <option value="ctrl">Ctrl / ⌘</option>
        </select>
      </label>
      <button @click="sort = []">Clear sort</button>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      v-model:sortedColumns="sort"
    />
    <div class="demo-log">Sort model: {{ describe(sort) }}</div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

type SortModel = Array<{ column: string | number; asc: boolean }>;

const rows = createEmployees(300);
// undefined until the first change, so the initial sort of the columns is kept
const sort = ref<SortModel>();
const multiSortKey = ref<"shift" | "ctrl">("shift");

const columns: Array<ColumnConfig<Employee>> = [
  // two initially sorted columns: department first, then salary
  { field: "department", text: "Department", sort: "asc" },
  { field: "salary", text: "Salary", type: "number", sort: "desc" },
  { field: "lastName", text: "Last name" },
  { field: "firstName", text: "First name" },
  { field: "role", text: "Role" },
  { field: "city", text: "City" },
];

const options = computed<GridOptions<Employee>>(() => ({
  multiSortKey: multiSortKey.value,
  defaultColumn: { sortable: true, flex: 1 },
}));

function describe(model: SortModel | undefined) {
  if (!model) return "department ↑, salary ↓ (initial)";
  return model.map((el) => `${el.column} ${el.asc ? "↑" : "↓"}`).join(", ") || "not sorted";
}
</script>
tsx
import { useMemo, useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions, type SortModel } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(300);

const columns: Array<ColumnConfig<Employee>> = [
  // two initially sorted columns: department first, then salary
  { field: "department", text: "Department", sort: "asc" },
  { field: "salary", text: "Salary", type: "number", sort: "desc" },
  { field: "lastName", text: "Last name" },
  { field: "firstName", text: "First name" },
  { field: "role", text: "Role" },
  { field: "city", text: "City" },
];

function describe(model: SortModel | undefined) {
  if (!model) return "department ↑, salary ↓ (initial)";
  return model.map((el) => `${el.column} ${el.asc ? "↑" : "↓"}`).join(", ") || "not sorted";
}

export default function SortingMulti() {
  // undefined until the first change, so the initial sort of the columns is kept
  const [sort, setSort] = useState<SortModel>();
  const [multiSortKey, setMultiSortKey] = useState<"shift" | "ctrl">("shift");

  const options = useMemo<GridOptions<Employee>>(
    () => ({ multiSortKey, defaultColumn: { sortable: true, flex: 1 } }),
    [multiSortKey]
  );

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          Multi sort key{" "}
          <select value={multiSortKey} onChange={(e) => setMultiSortKey(e.target.value as "shift" | "ctrl")}>
            <option value="shift">Shift</option>
            <option value="ctrl">Ctrl / ⌘</option>
          </select>
        </label>
        <button onClick={() => setSort([])}>Clear sort</button>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} sortedColumns={sort} onSortedColumnsChange={setSort} />
      <div className="demo-log">Sort model: {describe(sort)}</div>
    </div>
  );
}

Custom sort order ​

Use sortFunc when the alphabetical order makes no sense for the values, e.g. priorities (low → medium → high) or workflow states. The function receives the values (and rows) of two cells and returns a negative number, zero or a positive number for ascending order; the grid reverses the result for descending order.

The Key column doesn't need a sortFunc: the default comparator sorts TASK-9 before TASK-10 (natural order). Empty values – in Due (open tasks) the done tasks – are at the end in both directions.

Source
vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" />
</template>

<script setup lang="ts">
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";

const rows = createTasks(80);

const priorityOrder = ["low", "medium", "high"];
const statusOrder = ["todo", "in progress", "review", "done"];

const columns: Array<ColumnConfig<Task>> = [
  {
    id: "key",
    text: "Key",
    width: 90,
    // strings are compared in natural order: TASK-9 < TASK-10
    valueGetter: ({ data }) => `TASK-${data.id}`,
  },
  { field: "title", text: "Title", flex: 1, minWidth: 160 },
  {
    field: "priority",
    text: "Priority",
    // ascending = low → medium → high
    sortFunc: (a, b) => priorityOrder.indexOf(a.value) - priorityOrder.indexOf(b.value),
  },
  {
    field: "status",
    text: "Status",
    // workflow order instead of alphabetical order
    sortFunc: (a, b) => statusOrder.indexOf(a.value) - statusOrder.indexOf(b.value),
  },
  {
    id: "openDue",
    text: "Due (open tasks)",
    type: "date",
    width: 140,
    // empty values are always sorted to the end
    valueGetter: ({ data }) => (data.done ? null : data.due),
  },
];

const options: GridOptions<Task> = {
  rowId: (data) => data.id,
  defaultColumn: { sortable: true, width: 105 },
};
</script>
tsx
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";

const rows = createTasks(80);

const priorityOrder = ["low", "medium", "high"];
const statusOrder = ["todo", "in progress", "review", "done"];

const columns: Array<ColumnConfig<Task>> = [
  {
    id: "key",
    text: "Key",
    width: 90,
    // strings are compared in natural order: TASK-9 < TASK-10
    valueGetter: ({ data }) => `TASK-${data.id}`,
  },
  { field: "title", text: "Title", flex: 1, minWidth: 160 },
  {
    field: "priority",
    text: "Priority",
    // ascending = low → medium → high
    sortFunc: (a, b) => priorityOrder.indexOf(a.value) - priorityOrder.indexOf(b.value),
  },
  {
    field: "status",
    text: "Status",
    // workflow order instead of alphabetical order
    sortFunc: (a, b) => statusOrder.indexOf(a.value) - statusOrder.indexOf(b.value),
  },
  {
    id: "openDue",
    text: "Due (open tasks)",
    type: "date",
    width: 140,
    // empty values are always sorted to the end
    valueGetter: ({ data }) => (data.done ? null : data.due),
  },
];

const options: GridOptions<Task> = {
  rowId: (data) => data.id,
  defaultColumn: { sortable: true, width: 105 },
};

export default function SortingCustom() {
  return <Datagrid columns={columns} rows={rows} options={options} />;
}

Order of sort directions ​

sortingOrder defines which directions a click on the header cycles through. In a leaderboard the numbers should be sorted descending first, and the list should never fall back to the unsorted order: sortingOrder: ["desc", "asc"] on the grid applies it to all columns, a column can override it (here Player sorts A → Z first).

Source
vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" />
</template>

<script setup lang="ts">
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, createRandom } from "./data";

type Player = { player: string; team: string; points: number; wins: number; games: number };

const random = createRandom(3);
const rows: Array<Player> = createEmployees(40).map((e) => {
  const games = 20 + Math.floor(random() * 10);
  const wins = Math.floor(random() * games);
  const draws = Math.floor(random() * (games - wins));
  return { player: `${e.firstName} ${e.lastName}`, team: e.city, points: wins * 3 + draws, wins, games };
});

const columns: Array<ColumnConfig<Player>> = [
  // names: A → Z first (overrides the order of the grid)
  { field: "player", text: "Player", width: 200, sortingOrder: ["asc", "desc"] },
  { field: "team", text: "Team" },
  // initial sort: best players first
  { field: "points", text: "Points", type: "number", sort: "desc" },
  { field: "wins", text: "Wins", type: "number" },
  { field: "games", text: "Games", type: "number" },
];

const options: GridOptions<Player> = {
  // clicking a header: descending → ascending → descending … (never unsorted)
  sortingOrder: ["desc", "asc"],
  defaultColumn: { sortable: true, flex: 1 },
};
</script>
tsx
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, createRandom } from "../data";

type Player = { player: string; team: string; points: number; wins: number; games: number };

const random = createRandom(3);
const rows: Array<Player> = createEmployees(40).map((e) => {
  const games = 20 + Math.floor(random() * 10);
  const wins = Math.floor(random() * games);
  const draws = Math.floor(random() * (games - wins));
  return { player: `${e.firstName} ${e.lastName}`, team: e.city, points: wins * 3 + draws, wins, games };
});

const columns: Array<ColumnConfig<Player>> = [
  // names: A → Z first (overrides the order of the grid)
  { field: "player", text: "Player", width: 200, sortingOrder: ["asc", "desc"] },
  { field: "team", text: "Team" },
  // initial sort: best players first
  { field: "points", text: "Points", type: "number", sort: "desc" },
  { field: "wins", text: "Wins", type: "number" },
  { field: "games", text: "Games", type: "number" },
];

const options: GridOptions<Player> = {
  // clicking a header: descending → ascending → descending … (never unsorted)
  sortingOrder: ["desc", "asc"],
  defaultColumn: { sortable: true, flex: 1 },
};

export default function SortingOrder() {
  return <Datagrid columns={columns} rows={rows} options={options} />;
}

Sort order ​

The default comparator

  • compares numbers numerically and dates chronologically
  • compares strings in natural order ("item2" before "item10") and case insensitive
  • always puts empty values (null, undefined, "") at the end, independent of the sort direction

Options ​

OptionDescription
sortable (column)enables sorting
sort (column)initial sort direction "asc" or "desc"
sortingOrder (column / grid)order of directions when clicking, e.g. ["desc", "asc", null] or ["asc", "desc"] (never unsorted)
sortFunc (column)custom comparator (a, b) => number, a and b contain value and item
multiSortKey (grid)key for multi column sorting: "shift" (default) or "ctrl"

Binding the sort model ​

Bind the sort model when the sort is controlled outside of the grid, e.g. with a "Sort by" select like in an online shop, or to save it. The model contains the sorted columns in the sort order. In the demo the select sets the model and shows "Custom" when the user sorts with the headers:

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        Sort by
        <select v-model="sortBy">
          <option v-for="(preset, key) in presets" :key="key" :value="key">{{ preset.label }}</option>
          <option v-if="sortBy === 'custom'" value="custom">Custom (header click)</option>
        </select>
      </label>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      v-model:sortedColumns="sort"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createProducts, type Product } from "./data";

type SortModel = Array<{ column: string | number; asc: boolean }>;

const presets: Record<string, { label: string; sort: SortModel }> = {
  relevance: { label: "Relevance", sort: [] },
  priceAsc: { label: "Price: low to high", sort: [{ column: "price", asc: true }] },
  priceDesc: { label: "Price: high to low", sort: [{ column: "price", asc: false }] },
  rating: { label: "Best rated", sort: [{ column: "rating", asc: false }, { column: "price", asc: true }] },
  newest: { label: "Newest", sort: [{ column: "released", asc: false }] },
};

const rows = createProducts(200);
const sort = ref<SortModel>([]);

// the select shows the preset matching the sort model, "custom" after clicking a header
const sortBy = computed({
  get: () =>
    Object.keys(presets).find((key) => JSON.stringify(presets[key].sort) === JSON.stringify(sort.value)) ??
    "custom",
  set: (key: string) => (sort.value = presets[key].sort),
});

const columns: Array<ColumnConfig<Product>> = [
  { field: "name", text: "Product", flex: 1, minWidth: 160 },
  { field: "category", text: "Category" },
  { field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR") },
  { field: "rating", text: "Rating", type: "number" },
  { field: "released", text: "Released", type: "date" },
];

const options: GridOptions<Product> = {
  rowId: (data) => data.id,
  defaultColumn: { sortable: true, width: 110 },
};
</script>
tsx
import { useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type SortModel } from "@datagrid/react-ui";
import { createProducts, type Product } from "../data";

const presets: Record<string, { label: string; sort: SortModel }> = {
  relevance: { label: "Relevance", sort: [] },
  priceAsc: { label: "Price: low to high", sort: [{ column: "price", asc: true }] },
  priceDesc: { label: "Price: high to low", sort: [{ column: "price", asc: false }] },
  rating: { label: "Best rated", sort: [{ column: "rating", asc: false }, { column: "price", asc: true }] },
  newest: { label: "Newest", sort: [{ column: "released", asc: false }] },
};

const rows = createProducts(200);

const columns: Array<ColumnConfig<Product>> = [
  { field: "name", text: "Product", flex: 1, minWidth: 160 },
  { field: "category", text: "Category" },
  { field: "price", text: "Price", type: "number", valueFormatter: formatters.currency("EUR") },
  { field: "rating", text: "Rating", type: "number" },
  { field: "released", text: "Released", type: "date" },
];

const options: GridOptions<Product> = {
  rowId: (data) => data.id,
  defaultColumn: { sortable: true, width: 110 },
};

export default function SortingControlled() {
  const [sort, setSort] = useState<SortModel>([]);

  // the select shows the preset matching the sort model, "custom" after clicking a header
  const sortBy =
    Object.keys(presets).find((key) => JSON.stringify(presets[key].sort) === JSON.stringify(sort)) ?? "custom";

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          Sort by{" "}
          <select value={sortBy} onChange={(e) => setSort(presets[e.target.value].sort)}>
            {Object.entries(presets).map(([key, preset]) => (
              <option key={key} value={key}>
                {preset.label}
              </option>
            ))}
            {sortBy === "custom" && <option value="custom">Custom (header click)</option>}
          </select>
        </label>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} sortedColumns={sort} onSortedColumnsChange={setSort} />
    </div>
  );
}
ts
const sort = ref([{ column: "lastName", asc: true }]);
vue
<Datagrid v-model:sortedColumns="sort" ... />
tsx
const [sort, setSort] = useState<SortModel>([{ column: "lastName", asc: true }]);

<Datagrid sortedColumns={sort} onSortedColumnsChange={setSort} ... />

API ​

ts
api.sortColumn("salary", "desc");            // sort only this column
api.sortColumn("name", "asc", true);         // add to the existing sort
api.sortColumn("name", null);                // remove the sort of this column
api.toggleColumnSort("name", multiple);      // like a click on the header
api.setSortColumns([{ column: "a", asc: true }, { column: "b", asc: false }]);
api.getSortColumns();                        // [{ column: Column, asc: boolean }]
api.clearSort();

Sorting groups and tree data ​

  • Children of tree rows are sorted within their parent.
  • Group rows are sorted by the group key. When sorting by an aggregated column, groups are sorted by the aggregated value.

Events ​

@sort-change is emitted with the new sort model.

Released under the ISC License.