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.

  • :numbered shows page numbers and needs a count.
  • :load_more shows a button that appends the next page.
  • :infinite loads the next page on scroll.

The translations table pages through a long, sorted list and lets the reader pick a size:

elixir
pagination do
  type :numbered
  page_size 50
  page_size_options [20, 50, 100]
end

Gervaz 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:

elixir
pagination do
  type :load_more
  page_size 30

  ui do
    show_total false
  end
end

The 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.

elixir
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:

bash
?translations_filter_status=active&translations_sort=name:asc&translations_page=2

Give 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:

elixir
url_sync do
  mode :bidirectional
  params [:filters, :sort, :page, :search]
  prefix "section_drafts"
  max_filter_length 500
  preserve_params [:return_to]
end

The 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.

elixir
realtime do
  prefix "tag"

  visible fn record, user ->
    is_nil(user.site_id) or record.site_id == user.site_id
  end
end

The 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:

elixir
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"]
end

visible 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:

elixir
refresh do
  enabled true
  interval 30_000
end

The 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