# Recipe: A listing that lives in the URL > A Next.js-style web framework for Go: a folder under app/ is a route, HTML is written in Go, and nothing outside the standard library is imported. > /trilha/llms/recipes/listing.txt - [A listing that lives in the URL](/trilha/cookbook/listing): Page, ordering, filter and a queue that refreshes itself, with ListParams, ui.DataTable and ui.Poll. · 1632 tokens est. - [Live fragments](/trilha/reference/live): ui.Poll refreshes a fragment on a clock; ui.Live and ui.On refresh it when the server says so. · 1465 tokens est. - [Listings](/trilha/reference/listings): ListParams reads the state of a listing from the URL; ui.DataTable renders it. · 1938 tokens est. 3 page(s) · 5035 tokens est. in all --- The screen is `/documentos` in `examples/blog`: a table of documents with a search box, a type filter, sortable columns, pagination — and, above it, a block that keeps up with the processing queue on its own. Everything the visitor chooses is in the address, and the only script on the page is the kit's. ## What the screen reads The struct of the screen embeds `trilha.ListParams` and adds the filter only it has. `c.Bind` reads both from the query in one go and applies the limits of the listing (page at least 1, page size capped at `trilha.MaxPerPage`): ```go type consulta struct { trilha.ListParams Tipo string `form:"tipo"` } ``` ## What the table shows The columns are declared once. `Sort: true` does two things: it turns the header into a link, and it says the column may be ordered by — a `sort` in the address that is not one of these is dropped before the query, so the repository never receives a column name nobody declared. ```go var colunas = ui.Columns[documentos.Documento]{ {Key: "nome", Label: "Documento", Sort: true, Cell: func(d documentos.Documento) h.Node { return h.Text(d.Nome) }}, {Key: "tipo", Label: "Tipo", Cell: func(d documentos.Documento) h.Node { return ui.Badge(ui.Outline(), h.Text(d.Tipo)) }}, {Key: "tamanho", Label: "Tamanho", Sort: true, Num: true, Cell: func(d documentos.Documento) h.Node { return h.Text(d.Tamanho()) }}, {Key: "status", Label: "Status", Sort: true, Cell: func(d documentos.Documento) h.Node { return ui.Badge(h.Text(d.Status)) }}, } ``` ## One handler for the page and for the pieces `c.Fragment()` says who is asking. The whole page, the table alone when the kit swaps the listing, or the queue block at every tick of `ui.Poll`: ```go func Page(c *trilha.Ctx) (h.Node, error) { if c.Fragment() == "fila" { // O tique é o worker deste exemplo: a fila anda, e quando não sobra // nada o servidor encerra o polling na própria resposta. documentos.Andar() return fila(c), nil } var q consulta if err := c.Bind(&q); err != nil { return nil, err } q.Tipo = tipoValido(q.Tipo) tabela := lista(c, q) if c.Fragment() == "lista" { return tabela, nil } c.SetTitle("Documentos") return h.Div( ui.H1(h.Text("Documentos")), fila(c), // O resumo varre a lista inteira e é a parte lenta desta tela. Com o // Defer a página chega pronta sem ele, e ele entra sozinho um instante // depois; sem JavaScript, o placeholder carrega um link para a mesma // rota, que responde como página. ui.Defer(c, "resumo", "/documentos/resumo", ui.DeferOpts{Height: "9rem"}), tabela, ui.LiveScript(c), ), nil } ``` `ui.LiveScript(c)` is the one extra script, and only on the pages that watch something: `ui.Head` does not load `ui.live.js`. ## The table itself `ui.DataTable` builds the ordering links, the filter form, the pagination and the empty state from `ListState`. With `ID` set, all of them carry `ui.Swap("lista")`: ordering, filtering and paging swap the table instead of reloading — and with JavaScript off the same links navigate, because they are links. `Cards: true` is what keeps four columns readable on a phone: below 640px each row becomes a card, with the column's own label — `Columns[T].Label`, the same one the header already shows — next to its value, instead of a table that only scrolls sideways. ```go func lista(c *trilha.Ctx, q consulta) h.Node { docs, total := documentos.Buscar(documentos.Consulta{ Q: q.Q, Tipo: q.Tipo, Ordem: q.Sort, Asc: q.Asc(), Offset: q.Offset(), Limite: q.Limit(), }) return ui.DataTable(c, colunas, docs, ui.ListState{ Params: q.ListParams, Total: total, ID: "lista", Search: "Buscar no nome", Filters: filtro(q.Tipo), Caption: "Documentos recebidos", RowHref: func(i int) string { return "/documentos?q=" + docs[i].Nome }, // A listagem tem quatro colunas e é aberta no celular tanto quanto no // desktop: a linha vira cartão abaixo de 640px em vez de rolar de lado. Cards: true, // Without this the DataTable already draws a sensible empty state, and // it tells "no documents" from "no results for that term". This one is // here because the app speaks Portuguese and the kit's default does not. Empty: ui.Empty(ui.EmptyOpts{ Icon: "info", Title: "Nenhum documento com esse filtro", Hint: "Tente outro termo, ou limpe a busca.", }), }) } ``` The filter of this screen is an ordinary `