# Pagination and flash messages

`paginate` splits ordered query results into numbered pages and renders page
navigation. `flash` stores one-time messages, such as "Note created", in the
user's session and shows them on the next page.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/0.2.6/howto/pagination-and-messages/index.md.
Paginate [list route] with paginate.Parse and paginate.Query using a
deterministic Order, answer 400 for ErrInvalidPage and 404 for ErrPageNotFound,
and render paginate.Nav. After [form] succeeds, add a flash.Success message and
redirect; render flash.List on the next page. Add tests for both.
```

## Paginate a list

```go
func List(db *gorm.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        req, err := paginate.Parse(r, paginate.Options{PerPage: 20})
        if err != nil {
            response.Error(w, http.StatusBadRequest)
            return
        }
        page, err := paginate.Query[Note](r.Context(), db.Where("owner_id = ?", ownerID(r)).Order("created_at DESC, id DESC"), req)
        if errors.Is(err, paginate.ErrPageNotFound) {
            response.Error(w, http.StatusNotFound)
            return
        }
        if err != nil {
            response.Error(w, http.StatusInternalServerError)
            return
        }
        content := ui.Group(notesList(page.Items), paginate.Nav.Call(paginate.Navigation(page), ui.NoSlots{}))
        if err := (ui.Renderer{}).HTML(r.Context(), w, http.StatusOK, content); err != nil && !ui.Committed(err) {
            response.Error(w, http.StatusInternalServerError)
        }
    }
}
```

- `Parse` reads `?page=` (default 1). Zero, negative, malformed, repeated or
  larger-than-`MaxPageNumber` values return `ErrInvalidPage`. Set `SizeParam`
  (for example `"per_page"`) to let clients choose a size up to `MaxPerPage`
  (default 100); otherwise the size is always `PerPage` (default 20).
- `Query` requires an `Order` clause and returns `ErrUnordered` without one;
  unordered pages can repeat or skip rows. Add a unique column such as `id` as
  the last sort key. It counts and loads in one read-only `REPEATABLE READ`
  transaction, so the total and items agree. Inside your own transaction it uses
  that transaction instead.
- A page past the last returns `ErrPageNotFound`. Page 1 of an empty result is
  a normal empty page.
- Pagination applies no authorization: scope the query to rows the user may see.
- `Slice` paginates an in-memory slice the same way.

`Page` has `Items`, `Number`, `Size`, `Total`, `Pages`, `HasPrevious()`,
`HasNext()`, `Start()` and `End()` (1-based item positions, for "Showing 21–40
of 95").

## Page navigation

`paginate.Navigation(page)` builds props for `paginate.Nav`, a semantic,
unstyled component:

- a `<nav aria-label="Pagination">` landmark with Previous/Next links using
  `rel="prev"` and `rel="next"`;
- page numbers with `aria-current="page"` on the current page;
- long ranges elided with `…`, matching Django's
  `Paginator.get_elided_page_range`. Call `page.Links(onEachSide, onEnds)` for
  other widths.

Links are query-only relative URLs such as `?page=3&q=go`. They keep every other
query parameter, never contain a host or path, and page 1 omits `page=`. Style
the list with your own CSS, for example `nav[aria-label=Pagination] a[aria-current]`.

## Flash messages

Create the store from the same `security.Sessions` your browser sessions use.
In the standard project that is the session manager passed to
`service.Browser` in `project/application.go`:

```go
sessions := security.NewSessions(store, s.SecureCookies)
browser, err := service.Browser(sessions)
// ...
messages := flash.New(sessions)
```

Pass `messages` to your app constructors the same way you pass the database.
Routes must run inside session middleware: accounts pages and
`browser.Middleware` already do; for other routes add `sessions.LoadAndSave` to
`App.Middleware`.

```go
func Create(messages *flash.Messages) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // ... save the note ...
        if err := messages.Success(r.Context(), "Note created"); err != nil {
            response.Error(w, http.StatusInternalServerError)
            return
        }
        http.Redirect(w, r, "/notes/", http.StatusSeeOther)
    }
}
```

On the page that shows them, `Pop` returns and removes pending messages, so each
appears once:

```go
pending, err := messages.Pop(r.Context())
if err != nil {
    response.Error(w, http.StatusInternalServerError)
    return
}
content := ui.Group(flash.List.Call(flash.ListProps{Messages: pending}, ui.NoSlots{}), page)
```

- Levels are `flash.Info`, `Success`, `Warning` and `Error`. `flash.List`
  renders errors and warnings with `role="alert"` and the others with
  `role="status"`, adds `data-level` for styling, and renders nothing when there
  are no messages.
- Text is plain text and always escaped. Each message is 1–1000 bytes of UTF-8,
  and at most 20 messages can be pending per session.
- `Peek` reads without removing. Without session middleware, every call returns
  `flash.ErrNoSession`.

## Limits

- Pagination uses `OFFSET`, which slows down on very deep pages of large tables.
  Keyset ("load more after id X") pagination is not included.
- The standard project does not yet create the flash store for you; wire it as
  shown above.
