Paginate lists and show 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
Read https://cicadel.fauzaanu.com/en/0.2.1/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
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)
}
}
}Parsereads?page=(default 1). Zero, negative, malformed, repeated or larger-than-MaxPageNumbervalues returnErrInvalidPage. SetSizeParam(for example"per_page") to let clients choose a size up toMaxPerPage(default 100); otherwise the size is alwaysPerPage(default 20).Queryrequires anOrderclause and returnsErrUnorderedwithout one; unordered pages can repeat or skip rows. Add a unique column such asidas the last sort key. It counts and loads in one read-onlyREPEATABLE READtransaction, 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.
Slicepaginates 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 usingrel="prev"andrel="next"; - page numbers with
aria-current="page"on the current page; - long ranges elided with
…, matching Django’sPaginator.get_elided_page_range. Callpage.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:
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.
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:
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,WarningandError.flash.Listrenders errors and warnings withrole="alert"and the others withrole="status", addsdata-levelfor 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.
Peekreads without removing. Without session middleware, every call returnsflash.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.