Live pages and exports
How a table view follows changes as they happen, the events it emits, and export jobs.
A TableView can follow its rows as they change, emit events other blocks react to, and export its rows as a background job.
Export jobs
When the ExportRoutes are present, the Export button runs an asynchronous job, not a synchronous download: it exports the @Exported fields for the current filter/sort in the background, shows progress, and downloads the file on completion. Exports are CSV with download delivery, the only exporter and deliverer shipped.
- A job that exceeds its execution limit (10 minutes by default) is marked failed.
- Artifacts older than 24 hours are swept by a built-in cron.
- Jobs are private to the requesting user and tenant.
The engine behind this is reusable for your own exports. See Backend services.
Events & realtime
With realtime on (the default), the table broadcasts row mutations (created / updated / deleted) to other viewers, whose tables refresh live, and tracks edit presence: opening a row's edit form acquires a row lock, and other viewers see who is editing (with a warning dialog before a concurrent edit). Set realtime: false to opt a table out.
Rows can also change without a write through the table's routes, such as a batch import or a background job. Name the topic the backend publishes on in realtimeTopic (PublishMessage(topic, type)): the table reads its rows again on each event, keeping the current rows on screen. It also reads them again when the page asks its blocks to refresh (BlockFunctions.REFRESH_PAGE, refreshPageBlocks(), a page header button that changed something).
The other way round, a write through the table (a row or bulk action, a form it opened that saved, a row deleted, archived or restored) asks the whole page to refresh, not only the table: a KPI counting the same records, a tab set's badges or another list on the page read their data again with it. The refresh button and ⌘R read the table alone.
Live pages
On a page declared with live, the table's row events and its realtimeTopic go through the page's Direct / Pause valve like any block's: in Direct the table reads its rows again at most once per maxRate (instead of patching each row in place), paused it requests nothing and reads once on resume.
liveHold: true makes the pause visible on the table. While paused, its rows stay as they are and a pill laid over the first rows, just under the column headers (and under a sticky header band as its rows scroll), counts what arrived since: "12 new · Show". It counts the rows the table would list, not the events: while events come in, the table asks its own count route (the tab counters' one) how many rows its tab, search and filters match now, and shows how many more that is than the rows it holds, so a "4xx" tab announces only the new 4xx. Rows removed meanwhile lower that count; a count that fails falls back to the number of events. The page in Direct, the table also keeps its rows while they are on screen or scrolled past: a new row landing at the top would push down the rows the user reads, so the new ones wait behind the same pill, which takes no room and moves nothing. A table still below the screen follows the feed, and reads what it missed when it leaves the screen downwards. liveHold: { whileOnScreen: false } turns this off: in Direct the table follows the feed even on screen, as a short list of recent jobs that should show each one as it finishes, and holds only while the page is paused or for one of the reasons below. Show reads the rows once, the page staying paused, and so does an action of the user (a row action, the refresh button, refreshPageBlocks()), which also moves the hold to that moment. The user's own search, sort and pagination still read the rows: a source table's route receives liveHeldAt, the moment the rows were frozen, so it can answer them as they stood; a data controller's list has no such bound, and lists the rows as they are now.
The strip counts published rows, not messages: a row event counts each of its rows once, a batch announcing payload.count counts that many.
missedLabel words that count, given {count}, as the LiveToggle's own: liveHold: { missedLabel: "$api.live.new_requests" } reads "12 new requests · Show". While the page is paused, the strip also shows the breakdown a component gave the page with usePageLive().setMissedDetail(), in its tone, as the toggle does: "12 new requests · 2 in 5xx · Show". A table holding by itself while the page is live shows the count alone: the page's breakdown counts what the paused page missed.
An object holds the table alone while the rest of the page stays live, in more cases, and names them in a strip above the rows. All are off by default, so liveHold: true and {} behave as above:
liveHold: { whileRowOpen: true, whileScrolled: true, whileSearching: true, whileSorted: true }
whileRowOpen: while one of its rows is open in the side panel, so the row the user reads does not move away.whileSearching: while the user's search narrows the rows.whileSorted: while the user sorts the rows otherwise than by the table'sdefaultSort(the newest first, for a log), where new rows would land anywhere.whileScrolled: while the user has scrolled the table's first row out of view, reading further down.
While held this way the strip shows from the start and says why, before anything came in: "Table on hold · search active", then "Table on hold · search active · 3 new · Show". It appears on the user's own action, which is why it may take room; a count alone always comes as the pill. When several apply, the first of row open, search, sort and scroll is named. The table counts its events into the strip and Show reads its rows once; when the hold ends, it reads them if it missed any. While the page itself is paused, the strip names no reason: the page's LiveToggle says why it paused.
The rendered table also emits component events, TableViewEvents: ROW_CLICK, ROW_SELECT, ROW_ADD, ROW_EDIT, ROW_DELETE, FILTER_CHANGE, SORT_CHANGE, EXPORT, that other components can react to through watch-actions (Actions & reactivity). On the function side, TableViewFunctions ships one watchable id, CUSTOM_PAGE_FORM_SUCCESS (a url, optional preserveQuery): after a custom-page form succeeds, it navigates back to the table, restoring filters. {{params.X}} / {{query.X}} tokens in url are resolved against the current route first.