Share table state and update it live
v0.0.1
A Gervaz table can load rows in pages, keep its filters, sort and page in the address bar, update when a record changes anywhere, and reload on a timer. This guide sets up each one. The examples come from the translations, permissions, tags and drafts tables of Mishka CMS.
Before you begin
- Mount the table in a LiveView. The URL, realtime and refresh features each need a clause in that LiveView. See Mount a table and a form.
- Put the shared values on the domain: the PubSub module, the default page size and the default URL sync. A resource overrides any key.
Choose a pagination style
pagination takes type and the page sizes. The default page size is 20 and the default maximum page size is 150. Set type yourself. A domain pagination block without a type gives :load_more. A resource pagination block without a type, in a domain that has no pagination block, gives :numbered.
:numberedshows page numbers and needs a count.:load_moreshows a button that appends the next page.:infiniteloads the next page on scroll.
The translations table pages through a long, sorted list and lets the reader pick a size:
pagination do
type :numbered
page_size 50
page_size_options [20, 50, 100]
endGervaz checks these rules for a table that has an identity route, and reports a failure as a compile warning. page_size must be a positive integer and a member of page_size_options when both are set. Every option must be a positive integer. max_page_size must be at least the largest option, and it also clamps a page size that arrives in the URL. Without page_size_options, no page size selector is drawn.
show_total in the ui block defaults to true and runs a count query. Turn it off for a data layer that cannot count. The design tokens table of Mishka CMS has no data layer behind it, so it uses load more and no total:
pagination do
type :load_more
page_size 30
ui do
show_total false
end
endThe labels of the controls are in the same ui block: load_more_label, loading_text, prev_label, next_label, first_label, last_label and page_info_format.
Keep the state in the URL
Add a url_sync block so that a reload, a bookmark or a pasted link restores the view.
url_sync do
mode :bidirectional
params [:filters, :sort, :page, :page_size, :search]
prefix "translations"
end:read_only, the default mode, never rewrites the URL. :bidirectional also rewrites the URL as the reader filters, sorts and pages. params chooses what is synced from :filters, :sort, :page, :page_size, :search and :template. The default is [:filters, :sort, :page]. Add :page_size only when a page size selector exists.
Gervaz writes the params like this:
?translations_filter_status=active&translations_sort=name:asc&translations_page=2Give each table a distinct prefix. Two tables then share a page without clashing, and another page can link into this one. The reader can clear a filter that came from a link, because it is an ordinary filter. max_filter_length ignores values longer than the limit, 500 by default. preserve_params keeps params the table does not know when it rewrites the URL. The Mishka Document drafts table keeps return_to:
url_sync do
mode :bidirectional
params [:filters, :sort, :page, :search]
prefix "section_drafts"
max_filter_length 500
preserve_params [:return_to]
endThe LiveView must decode the URL and pass url_state to the component. Mount a table and a form shows the handle_params/3 clause.
Update rows over PubSub
A realtime block makes the table subscribe to the topics of one resource and apply every change it sees. prefix is required while realtime is on. pubsub usually comes from the domain.
realtime do
prefix "tag"
visible fn record, user ->
is_nil(user.site_id) or record.site_id == user.site_id
end
endThe table subscribes to "<prefix>:created", "<prefix>:updated" and "<prefix>:destroyed". For a user with a site_id, it also subscribes to "<prefix>:<site_id>:created", "<prefix>:<site_id>:updated" and "<prefix>:<site_id>:destroyed". The Ash pub_sub notifier of the resource must publish on those topics:
pub_sub do
module Application.compile_env!(:mishka_cms_core, :endpoint)
prefix "tag"
publish :create, [[nil, :site_id], "created"]
publish :update, [[nil, :site_id], "updated"]
publish :destroy, [[nil, :site_id], "destroyed"]
endvisible decides, for a user who is not a master, whether a record that arrives may be inserted. A master always passes. Without visible, Gervaz compares the tenant attribute of the record with the user's. A function you write replaces that comparison, so it must keep other tenants out itself. A row that arrives over realtime is also checked against the table's own filters, search and path params. One that does not match is not inserted, and one edited out of the filters leaves the list.
The broadcasts arrive at the LiveView, not at the component. Forward them with send_update/2 as Mount a table and a form shows. After each notification the table counts its total again, so a table that keeps a count follows added and removed rows. A table that keeps a count is a numbered table, or any table with show_total true. To take over an update, use the on_realtime hook and return {:halt, socket}.
Set enabled false on a resource to turn off a realtime default of the domain. The resource wins.
Reload on a timer
Prefer realtime when the data is written through Ash with a pub_sub notifier. It pushes one update for a real change, and a timer runs one query per tick for every connected reader. For data that changes without a broadcast, add a refresh block:
refresh do
enabled true
interval 30_000
endThe table sends :gervaz_refresh to the parent LiveView on the interval, and the LiveView must forward it. The component then reloads page 1 and schedules the next tick. The domain default is 30 seconds. Only enabled and interval change what the table does. pause_on_interaction, show_indicator and pause_on_blur are stored in the config, but the built-in table does not act on them. MishkaGervaz.Table.Web.Refresh has pause/1 and resume/2 for code that wants to pause the timer.
Next steps
- Table pagination and updates DSL: every option of these four blocks.
- React to table and form events with hooks:
on_realtimeandon_load. - How domain defaults work: set the shared values once.