Live demos
Three working examples to try in context. Each one is built only from the Mist kit and the xoong controls; the numbers in them are made up.
- Admin appA full-page back office: dashboard, an articles table with sort, select and bulk actions, settings, notifications and a command palette on Ctrl or Cmd plus K.Open the full-page demo
- Landing siteA marketing page: hero, pricing with a monthly and yearly switch, testimonials, FAQ, a sign-up form with validation and a cookie consent bar.Open the full-page demo
- People adminA data table, an add dialog and bulk actions on a fake API that lives in this page, with no application code. It is right below, with its code.Jump to it
The two full pages have their own layout. A small bar at the bottom left of each one leads back to the docs and to the other demo.
People admin
A small people admin on the in-page API: sort, search, page, add someone, delete one or several. It is a table, a dialog with a generated form, and two buttons; there is no application code. Changes last until you reload.
<xo-view get="listPeople">
<p class="toolbar"><button xo-open="#add">Add person</button></p>
<xo-table columns="name,email:E-mail,role,topics:Topics" sortable filters="name:Search by name" column-picker selectable page-size="5" empty="Nobody here">
<span slot="role" class="badge">{role}</span>
<span slot="topics"><span class="badge" xo-each="topics">{.}</span></span>
<button class="danger" xo-click="deletePerson" xo-confirm="Delete {name}?" xo-confirm-ok="Delete" xo-notice="Deleted">Delete</button>
<button slot="bulk" class="danger" xo-click="deletePerson" xo-selected xo-confirm="Delete the ticked people?" xo-confirm-ok="Delete">Delete ticked</button>
</xo-table>
</xo-view>
<xo-dialog id="add" heading="Add person">
<xo-view><xo-form op="createPerson" submit="Add"><button type="button" xo-close>Cancel</button></xo-form></xo-view>
</xo-dialog>// The demo pages: the data layer, the admin parts and the rich widgets, pointed at the in-page API.
import './api.js';
import '@xoong/data/view';
import '@xoong/ui';
import '@xoong/forms';
import '@xoong/controls';
import '@xoong/richtext';// A backend for the demos, in the page: GitHub Pages serves files only, so a function plays the API.
// The data layer takes any `fetch` (configure({ fetch })), so everything on the demo pages goes through the same code path
// as a real server: typed operations, caching, loading and error states. Data lives in memory: reload to start again.
import { configure } from '@xoong/data';
import operations from '/.xoong/operations.json' with { type: 'json' };
const people = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@example.org', role: 'admin', topics: ['web', 'data'], keywords: ['analysis', 'engine'], bio: '<p>Wrote the first program for the <strong>Analytical Engine</strong>.</p>' },
{ id: 2, name: 'Grace Hopper', email: 'grace@example.org', role: 'editor', topics: ['ops'], keywords: ['compiler'], bio: '<p>Built the first compiler.</p>' },
{ id: 3, name: 'Edsger Dijkstra', email: 'edsger@example.org', role: 'viewer', topics: ['data'], keywords: ['graphs', 'semaphores'], bio: '' },
{ id: 4, name: 'Barbara Liskov', email: 'barbara@example.org', role: 'editor', topics: ['design', 'data'], keywords: ['abstraction'], bio: '' },
{ id: 5, name: 'Donald Knuth', email: 'donald@example.org', role: 'viewer', topics: ['data'], keywords: ['literate programming'], bio: '' },
{ id: 6, name: 'Margaret Hamilton', email: 'margaret@example.org', role: 'admin', topics: ['ops'], keywords: ['software engineering'], bio: '' },
{ id: 7, name: 'Tim Berners-Lee', email: 'tim@example.org', role: 'editor', topics: ['web'], keywords: ['http', 'html'], bio: '' },
{ id: 8, name: 'Linus Torvalds', email: 'linus@example.org', role: 'viewer', topics: ['ops'], keywords: ['kernel'], bio: '' },
];
let nextId = people.length + 1;
const json = (status, body) => new Response(body === undefined ? null : JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
const problem = (status, code, message, fields) => json(status, { error: { code, message, ...(fields ? { fields } : {}) } });
const clean = (b) => ({
name: String(b.name ?? '').trim(), email: String(b.email ?? '').trim(), role: b.role || 'viewer',
topics: Array.isArray(b.topics) ? b.topics : [], keywords: Array.isArray(b.keywords) ? b.keywords : [], bio: b.bio ?? '',
});
let failed = false; // the demo of an error state: the first search for "fail" is refused, the retry works
function list(q) {
if (!failed && q.get('name') === 'fail') { failed = true; return problem(500, 'unavailable', 'The people service is not answering'); }
let rows = people.filter((p) => (!q.get('name') || p.name.toLowerCase().includes(q.get('name').toLowerCase())) && (!q.get('role') || p.role === q.get('role')));
const sort = q.get('sort');
if (sort) {
const key = sort.replace(/^-/, ''), dir = sort.startsWith('-') ? -1 : 1;
rows = [...rows].sort((a, b) => (typeof a[key] === 'number' ? a[key] - b[key] : String(a[key] ?? '').localeCompare(String(b[key] ?? ''))) * dir);
}
const offset = Number(q.get('offset') || 0), limit = Number(q.get('limit') || rows.length);
return json(200, { items: rows.slice(offset, offset + limit), total: rows.length });
}
async function handle(url, init = {}) {
const { pathname, searchParams } = new URL(url, location.origin);
const method = (init.method || 'GET').toUpperCase();
await new Promise((r) => setTimeout(r, 120)); // a little latency, so loading states are visible
const m = pathname.match(/^\/api\/people(?:\/(\d+))?$/);
if (!m) return problem(404, 'not_found', 'No such route');
const id = m[1] && Number(m[1]);
const found = id ? people.find((p) => p.id === id) : null;
if (!id && method === 'GET') return list(searchParams);
if (!id && method === 'POST') {
const body = clean(JSON.parse(init.body));
if (people.some((p) => p.email === body.email)) return problem(409, 'email_taken', `${body.email} is already used. Try another address.`);
const person = { id: nextId++, ...body };
people.push(person);
return json(201, person);
}
if (!found) return problem(404, 'not_found', `Person ${id} does not exist`);
if (method === 'GET') return json(200, found);
if (method === 'PUT') {
const body = clean({ ...found, ...JSON.parse(init.body) });
if (people.some((p) => p.id !== id && p.email === body.email)) return problem(409, 'email_taken', `${body.email} is already used. Try another address.`);
Object.assign(found, body);
return json(200, found);
}
if (method === 'DELETE') { people.splice(people.indexOf(found), 1); return json(204); }
return problem(405, 'method_not_allowed', 'Not allowed');
}
configure({ api: operations, baseUrl: '', fetch: handle });The api.js tab is the whole backend of this page. Replace fetch: handle with a baseUrl and the same markup talks to a real server.