xoong
Menu · Data and tables
DocsGuidesData and tables

Data and tables

Reads are <xo-view get>. Writes are xo-click and xo-submit. The data layer does the caching, retries, loading and errors.

Start-up

This is all the JavaScript most apps need. operations.json is generated by xoong sync; baseUrl is where your API lives.

import { configure } from '@xoong/data';
import '@xoong/data/view';
import '@xoong/ui';
import operations from '/.xoong/operations.json' with { type: 'json' };

configure({ api: operations, baseUrl: '/api' });
npx xoong sync openapi.json

A table in one element

<xo-table> draws a table from the view's data. The attributes below turn on sorting (sort query parameter), a search box per filter (the field's query parameter), row selection, bulk actions and paging (limit, offset and total in the response). Children fill cells: slot="role" replaces the role cell, any other element goes in the last cell.

{role}
<xo-view get="listPeople">
  <xo-table columns="name,email:E-mail,role" sortable filters="name:Search by name" selectable page-size="5" empty="Nobody here">
    <span slot="role" class="badge">{role}</span>
    <button class="danger" xo-click="deletePerson" xo-confirm="Delete {name}?" xo-notice="Deleted">Delete</button>
    <button slot="bulk" class="danger" xo-click="deletePerson" xo-selected>Delete ticked</button>
  </xo-table>
</xo-view>
AttributeWhat it does
columnsfield, field:Label or field:Label:format, comma separated
sortableHeader buttons set sort (name, then -name)
filtersname:Search by name: a search box that sets that query parameter
selectableA checkbox per row; slot="bulk" buttons run once per ticked row
page-sizeSets limit and shows Previous and Next (offset)
emptyThe text shown when there are no rows

Your own markup

When a table is not the right shape, write the markup. xo-each repeats an element for each item, xo-if shows it conditionally, {name} reads a field, and xo-param sets a query parameter and refetches (the button for the current value gets aria-current). Text is always escaped.

Loading…

{$error.code}: {$error.detail}

<xo-view get="listPeople">
  <p class="toolbar">
    <button xo-param="role=">Everyone</button>
    <button xo-param="role=admin">Admins</button>
    <button xo-param="role=editor">Editors</button>
    <button xo-param="role=viewer">Viewers</button>
  </p>
  <p xo-if="$loading" aria-busy="true">Loading…</p>
  <p xo-if="$error" role="alert">{$error.code}: {$error.detail}</p>
  <ul class="people">
    <li xo-each="items">
      <strong>{name}</strong> <span class="badge">{role}</span><br>
      <a href="mailto:{email}">{email}</a>
    </li>
  </ul>
</xo-view>

Loading, empty and error

A view tells you in which state it is: $loading while a request runs, $error when it failed, and an empty list otherwise. <xo-table> shows its empty content only when the list is really empty (not while loading, not after an error); a <template slot="empty"> gives it any markup. xo-refresh on a button fetches the view's data again: a "Try again". Say what happened, what to do, and offer one action.

Try it: type zzz for an empty list, or fail: the first request with it fails, and Try again then succeeds.

Loading people
<xo-view class="states" get="listPeople" p-limit="4">
  <div class="skeleton" xo-if="$loading" role="status" aria-busy="true">
    <span class="xo-visually-hidden">Loading people</span>
    <div class="sk-head"><i></i><i></i><i></i><i></i><i></i></div>
    <div class="sk-row"><i></i><i></i><i></i><i></i><i></i></div><div class="sk-row"><i></i><i></i><i></i><i></i><i></i></div><div class="sk-row"><i></i><i></i><i></i><i></i><i></i></div><div class="sk-row"><i></i><i></i><i></i><i></i><i></i></div><div class="sk-row"><i></i><i></i><i></i><i></i><i></i></div>
  </div>
  <div class="state error" xo-if="$error" role="alert">
    <svg class="art" viewBox="-2 -2 68 68" aria-hidden="true"><g transform="rotate(-10 32 40)"><rect class="m-ink" x="8" y="22" width="48" height="7.5" rx="3.75"/><rect class="m-ink" x="1" y="35" width="12" height="6.5" rx="3.25"/><rect class="m-ink" x="51" y="35" width="12" height="6.5" rx="3.25"/><path class="m-body" d="M12 31.5h40V44a12 12 0 0 1-12 12H24A12 12 0 0 1 12 44Z"/><path d="M34 31.5l-4 8 6 4-5 9" fill="none" stroke="var(--bg)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></g><path class="m-steam" d="M44 6l6 6-6 6"/></svg>
    <h3>Could not load people</h3>
    <p><code>{$error.code}</code> {$error.detail}. Check your connection and try again.</p>
    <div class="state-actions"><button class="primary" type="button" xo-refresh><span class="ic" data-ic="redo" aria-hidden="true"></span>Try again</button></div>
  </div>
  <xo-table columns="name,email:E-mail" filters="name:Search (try zzz or fail)" empty="Nobody here">
    <template slot="empty"><div class="state"><svg class="art" viewBox="-2 -2 68 68" aria-hidden="true"><rect class="m-ink" x="1" y="35" width="12" height="6.5" rx="3.25"/><rect class="m-ink" x="51" y="35" width="12" height="6.5" rx="3.25"/><path class="m-body" d="M12 31.5h40V44a12 12 0 0 1-12 12H24A12 12 0 0 1 12 44Z"/><ellipse cx="32" cy="31.5" rx="20" ry="4.5" fill="none" stroke="currentColor" stroke-width="3"/><rect class="m-ink" x="9" y="8" width="48" height="7.5" rx="3.75" transform="rotate(-14 33 12)"/><path class="empty-body" d="M20 20h24"/></svg><h3>Nobody here yet</h3><p>No one matches this search.</p><div class="state-actions"><button type="button" xo-param="name=">Clear search</button></div></div></template>
  </xo-table>
</xo-view>

Actions

A button with xo-click calls an operation by name (or as METHOD /path). {id} and other path parameters come from the row. After it succeeds, the queries it affects are refetched. These attributes shape the call:

AttributeWhat it does
xo-confirm="Delete {name}?"Asks first, in a native dialog
xo-notice="Saved"Shows a toast on success; errors always show one
xo-with="role=admin"Adds fields to the request body
xo-optimistic="role=admin"Applies the change at once, undone if the call fails
xo-selectedRuns once per ticked row (or sends the ticked ids in one request)

Errors and loading

A running call sets aria-busy on the button. A failure becomes one error type with status, code and detail (from application/problem+json, { error: { code, message } } or { detail }), shown by <xo-toast> or by $error in your markup. Listen for xo-done and xo-error to react in code.

<xo-view get="getPerson" p-id="99">
  <p xo-if="$error" role="alert">{$error.status} {$error.code}: {$error.detail}</p>
</xo-view>
<xo-toast></xo-toast>
document.addEventListener('xo-error', (e) => console.warn(e.detail.code, e.detail.detail));