Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

Lists that load more

if (rows.HasMore) { Button("Load more", onPress: () => rows.LoadMore()); }

A page's list over a big table holds the first answer's rows (about 2 MB of them). rows.HasMore says whether the set holds rows past what the page has, and rows.LoadMore() reads the next answer and appends it. DataGrid pages by itself, LoadMoreButton(rows) puts the same offer under a list you draw with foreach, and a list that is every row it will ever have offers nothing.

stable3 examples compiled by CIuiquerydatalimits

Summary#

A page's query is sent in answers of about 2 MB of rows (How many rows a page is sent), so a list over a table bigger than that holds the first answer — and looks complete. Two members of the list say otherwise: rows.HasMore is true while rows exist past what the page holds, and rows.LoadMore() reads the next answer, from the row after the last one held, and appends it. The list grows in place; every slot that reads it re-renders.

entity Reading {
  [Required, MaxLength(40)] string Station;
  DateTime TakenAt;
  decimal Value;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

[Route("/readings")] [Render(CSR)] [AllowAnonymous]
component ReadingsPage() {
  live var readings = Reading.OrderByDescending(r => r.TakenAt).ToList();

  render {
    Stack(gap: 1, p: 6) {
      foreach (var r in readings) { Text($"{r.Station}: {r.Value}"); }
      if (readings.HasMore) { Button("Load more", onPress: () => readings.LoadMore()); }
    }
  }
}

Signature#

rows.HasMore                    // bool — the set holds rows past what this list holds
rows.LoadMore();                // read the next answer and append it (awaited in a body)
LoadMoreButton(rows);           // the kit's offer: drawn only while rows.HasMore
DataGrid("…", rows: rows, …);   // pages by itself — nothing to add

Description#

The kit's controls page by themselves#

DataGrid draws Load more under its rows whenever the rows it was handed hold more, so a table over a page's query pages with nothing written. A list you draw yourself takes LoadMoreButton(rows) under it — the same offer, gone once the last answer is in:

[Route("/stations")] [Render(CSR)] [AllowAnonymous]
component StationsPage() {
  live var readings = Reading.OrderBy(r => r.Station).ToList();

  render {
    Stack(gap: 4, p: 6) {
      DataGrid("Readings", rows: readings,
               columns: [ new GridColumn<Reading> { Name = "Station", Label = "Station", Value = r => r.Station } ]);
      Stack(gap: 1) {
        foreach (var r in readings) { Text(r.Station); }
        LoadMoreButton(readings, label: "More readings");
      }
    }
  }
}

⚠ A DataGrid's search, column filters and sort work on the rows it has loaded. To search the whole table, put the search in the page's query (Reading.Where(r => r.Station.Contains(find))), which the database answers over every row.

Asked of the list, wherever it travels#

HasMore is a question about the value, not about how it was declared — which is why a control handed a page's query as a plain T[] rows can ask it. A list that is not a window of anything answers false, and LoadMore() on it does nothing: an array built in memory, a server function's answer, a list computed from a query (readings.Where(r => r.Value > 0) — ask readings, not the list made from it), or a .Take(n) the page has filled. That is the true answer for each: it is every row it will ever have.

In an action, it is awaited#

LoadMore() in an action body waits for the next answer, so the statement after it sees the rows it appended:

[Route("/readings/count")] [Render(CSR)] [AllowAnonymous]
component ReadingCount() {
  live var readings = Reading.OrderBy(r => r.TakenAt).ToList();
  int held = 0;

  action More() {
    readings.LoadMore();
    held = readings.Count;
  }

  render {
    Stack(gap: 2, p: 6) {
      Text($"{held} loaded");
      Button("Load the next readings", onPress: More);
    }
  }
}

Where can it not be written?#

In a render slot, LoadMore() is refused: a slot re-runs whenever what it reads changes, so a read written there would run on every render. Call it from a press. In code that runs on the server, both are refused: a function's list is every row its query names — server code is never windowed — so there is nothing more to ask for. Bound a server read with .Take(n) and .Skip(n).

How big is the next answer?#

As large as the answer budget allows — about 2 MB of rows, measured as they are sent — so a page that Selects the few fields it shows (Select the fields a page shows) gets more rows a press. The next answer starts at the row after the last one held, so paging never skips or repeats a row. When the set changes while a page holds it (a live list re-reads after a commit), the re-read keeps every row already loaded.

See also#

Related

How many rows a page is sent

A page's query is sent bounded. Its own .Take(n) is the ceiling: the browser may ask for fewer of those rows, never…

A list or table with no bound

osy lint's ui-unbounded-list-does-not-page (SHOULD) fires when a page draws a list with foreach from a query that reads…

A picker over every row — the SearchPicker

A Dropdown is usable for a few dozen options; over an entity that grows, past the first answer (about 2 MB of rows) the…

Select the fields a page shows

osy lint's cost-page-ships-whole-rows-for-few-fields (CONSIDER) fires when a page reads whole rows of a wide entity and…

The reactivity & lifecycle model

How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are…