# Sorting by a column the user picks

> A sortable table names its sort key with the chosen column's own selector, never with a string. Over rows already loaded the sort happens in memory; over a PAGED read the server does it, because ordering a page you already hold is a different question from ordering the set and then taking a page.

<!-- id: ui-sort-by-column · area: ui · stability: preview · html: https://osysharp.com/reference/ui/sort-by-column/ -->

## Summary        {#summary}
A column already knows how to read its value — that is what its selector is. So the chosen **column** names the
sort, and no string key ever enters the picture:

```osy title="click a header, sort by that column" test app=ui-sort-by-column
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }

// The rows are PASSED IN, so they are already fetched and sorting them is a local matter.
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
  Column<T> sortBy = columns[0];
  action SortBy(Column<T> c) { sortBy = c; }

  render {
    Stack(gap: 2) {
      Row(gap: 2) {
        foreach (var h in columns) {
          Pressable(onClick: () => SortBy(h)) { Text(h.Label, fontWeight: "600"); }
        }
      }
      foreach (var r in rows.OrderBy(sortBy.Value)) { Text(sortBy.Value(r)); }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  live var reports = Report.ToList();
  render {
    Grid(rows: reports, columns: [ new Column<Report> { Label = "Title", Value = r => r.Title },
                                   new Column<Report> { Label = "Total", Value = r => r.Total.ToString() } ]);
  }
}
```

A string key would rename silently and fail at run time. A selector is checked against the row type, so a column
that reads a field the row does not have is a compile error naming the row type.

## Signature      {#signature}

| form | where it runs |
|---|---|
| `rows.OrderBy(column.Value)` | in memory, over rows the client already holds |
| `rows.OrderByDescending(column.Value)` | in memory, descending |
| `Entity.OrderBy(column.Value).Take(n)` | on the SERVER, before the page is taken |

## Description    {#description}

### Loaded rows sort in memory   {#in-memory}
While the page **is** the table — every row is loaded — sorting the rows in hand sorts the table. `OrderBy` over a
list or a component's `T[]` parameter does exactly that, and nothing crosses the network.

Where the rows come from decides which sort you get, and it is not a detail. Rows **passed in** as a parameter have
already been fetched, so there is no read left to change and the sort is local. `OrderBy` written directly on a
**read** — an entity set, or a `live var` holding one — folds into that read instead, so the SERVER sorts. That is
what you want (sorting a page you were handed is the wrong question), and it is why the paged form below is the
same expression rather than a different one.

### A PAGED read sorts on the server   {#paged}
Once a read is paged, sorting the rows in hand is the wrong question: it re-orders the twenty rows you were given,
when what you asked for is the first twenty **of the ordered set**. Write the same expression on the entity read and
the server orders first:

```osy title="the server orders, then takes the page" test app=ui-sort-by-column-paged
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }

[Page("/")] [AllowAnonymous]
component Home() {
  var columns = [ new Column<Report> { Label = "Title", Value = r => r.Title },
                  new Column<Report> { Label = "Total", Value = r => r.Total.ToString() } ];
  Column<Report> sortBy = columns[0];
  action SortBy(Column<Report> c) { sortBy = c; }

  live var page = Report.OrderBy(sortBy.Value).Take(20);

  render {
    Stack(gap: 2, p: 4) {
      Row(gap: 2) {
        foreach (var h in columns) {
          Pressable(onClick: () => SortBy(h)) { Text(h.Label, fontWeight: "600"); }
        }
      }
      foreach (var r in page) { Text(sortBy.Value(r)); }
    }
  }
}
```

Changing the column re-runs the read, so the page can gain and lose rows — which is the point, and the difference
you can see: the first twenty by name and the first twenty by total are not the same twenty.

### Two rules the compiler enforces on a paged sort   {#rules}
A paged sort is compiled ahead of time, so the columns it can sort by have to be knowable when it is compiled.

1. **The chosen column must be taken from the column list** — `sortBy = columns[0]`. That one line is what lets the
   compiler read every column the sort could use.
2. **The column list must be written out** where the component declares it. A list built by a function, or arriving
   as a parameter, has no contents to read, and a sort key guessed at would quietly return a different page.

Anything else is refused, naming what to change. Your own code never mentions how the choice reaches the server.

### Sorting state belongs beside the read   {#state}
For a paged sort the chosen column and the read live in the same component, because the read is what the sort
belongs to. A reusable grid takes the chosen column and a "sort by this" callback as parameters.

## Examples       {#examples}

```osy title="descending, and a computed sort key" test app=ui-sort-by-column-computed
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }

// A selector is an ordinary expression, so a column can sort by something it COMPUTES rather than a stored field —
// which is the thing a string key could never have expressed.
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
  Column<T> sortBy = columns[0];
  render {
    Stack(gap: 2) {
      foreach (var r in rows.OrderByDescending(sortBy.Value)) { Text(sortBy.Value(r)); }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  live var reports = Report.ToList();
  render {
    Grid(rows: reports, columns: [ new Column<Report> { Label = "Band", Value = r => r.Total > 150 ? "high" : "low" },
                                   new Column<Report> { Label = "Title", Value = r => r.Title } ]);
  }
}
```

## See also       {#see-also}
- [Func<T, R>](https://osysharp.com/reference/ui/function-value/) — what a column's `Value` is, and why a selector rather than a string
- [generic component](https://osysharp.com/reference/ui/generic-component/) — one `Column<T>` serving every row type
- [component](https://osysharp.com/reference/ui/component/) — state, actions and the render block
