{ "tasks": [ { "id": "1", "text": "Read the thread", "done": false }, { "id": "2", "text": "Write the task list", "done": false }, { "id": "3", "text": "Ship it", "done": false } ], "nextId": 4, "draft": "", "refusing": false}{ "remove": [], "place": [ { "key": "3", "in": "tasks", "before": null }, { "key": "2", "in": "tasks", "before": "3" }, { "key": "1", "in": "tasks", "before": "2" } ]}moveBefore. Your browser has it.app.js, one of the first three files, which are the three ways to render, and the few lines of main.js that hold the state and take a message. The rest is the instruments of this page: they count, measure and show.import { splitTemplate, makeNode, readParts, readPlaces, plan, perform, writeItem, listen } from '../../dist/baremirror-alpha.js';import { requests } from '../../dist/requests.js';const row = splitTemplate('row', ['li', ['x-checkbox', { checked: task => task.done, 'aria-label': task => task.text, on: { 'x-checkbox-change-request': 'toggle' } }], ['span', { class: 'text' }, task => task.text], ['input', { class: 'note', placeholder: 'A note, not saved', 'aria-label': 'Note' }], ['x-badge', { variant: task => task.tone, text: task => task.status }], ['x-button', { size: 'sm', variant: 'danger', on: { press: 'remove' } }, 'Remove']]);function made(task) { const node = makeNode(row.fixed); writeItem(node, row, task); return node;}export function mount(root, dispatch) { listen(root, { dispatch, requests, events: row.events }); return vm => { const { tasks, done } = readParts(root); const reading = readPlaces({ tasks: { parent: tasks }, done: { parent: done } }); const steps = plan(reading.places, vm.places); const nodes = perform(reading, steps, key => made(vm.rows[key])); Object.entries(nodes).forEach(([key, node]) => writeItem(node, row, vm.rows[key])); return steps; };}const entities = { '&': '&', '<': '<', '>': '>', '"': '"' };const escaped = text => text.replace(/[&<>"]/g, character => entities[character]);const row = task => ` <li data-id="${task.id}"> <x-checkbox ${task.done ? 'checked' : ''} aria-label="${escaped(task.text)}"></x-checkbox> <span class="text">${escaped(task.text)}</span> <input class="note" placeholder="A note, not saved" aria-label="Note"> <x-badge variant="${task.tone}" text="${task.status}"></x-badge> <x-button size="sm" variant="danger">Remove</x-button> </li>`;const idOf = event => event.target.closest('li').dataset.id;function listen(list, dispatch) { list.addEventListener('x-checkbox-change-request', event => { event.preventDefault(); dispatch(['toggle', idOf(event)]); }); list.addEventListener('press', event => dispatch(['remove', idOf(event)]));}export function mount(root, dispatch) { const lists = { tasks: root.querySelector('[data-x-part="tasks"]'), done: root.querySelector('[data-x-part="done"]'), }; Object.values(lists).forEach(list => listen(list, dispatch)); return vm => Object.entries(vm.places).forEach(([name, ids]) => { lists[name].innerHTML = ids.map(id => row(vm.rows[id])).join(''); });}const template = document.createElement('template');template.innerHTML = ` <li> <x-checkbox></x-checkbox> <span class="text"></span> <input class="note" placeholder="A note, not saved" aria-label="Note"> <x-badge></x-badge> <x-button size="sm" variant="danger">Remove</x-button> </li>`;function create(id) { const li = template.content.firstElementChild.cloneNode(true); li.dataset.id = id; return { li, box: li.querySelector('x-checkbox'), text: li.querySelector('.text'), badge: li.querySelector('x-badge') };}function update({ box, text, badge }, task) { if (box.hasAttribute('checked') !== task.done) box.toggleAttribute('checked', task.done); if (box.getAttribute('aria-label') !== task.text) box.setAttribute('aria-label', task.text); if (text.textContent !== task.text) text.textContent = task.text; if (badge.getAttribute('variant') !== task.tone) badge.setAttribute('variant', task.tone); if (badge.getAttribute('text') !== task.status) badge.setAttribute('text', task.status);}function place(list, node, before) { if (list.moveBefore && node.isConnected) list.moveBefore(node, before); else list.insertBefore(node, before);}function arrange(list, nodes) { const wanted = new Set(nodes); const following = node => { let next = node.nextElementSibling; while (next && !wanted.has(next)) next = next.nextElementSibling; return next; }; nodes.reduceRight((next, node) => { if (node.parentNode !== list || following(node) !== next) place(list, node, next); return node; }, null);}const idOf = event => event.target.closest('li').dataset.id;function listen(list, dispatch) { list.addEventListener('x-checkbox-change-request', event => { event.preventDefault(); dispatch(['toggle', idOf(event)]); }); list.addEventListener('press', event => dispatch(['remove', idOf(event)]));}export function mount(root, dispatch) { const lists = { tasks: root.querySelector('[data-x-part="tasks"]'), done: root.querySelector('[data-x-part="done"]'), }; const rows = new Map(); const rowOf = id => { if (!rows.has(id)) rows.set(id, create(id)); return rows.get(id); }; Object.values(lists).forEach(list => listen(list, dispatch)); return vm => { rows.forEach((row, id) => { if (!vm.rows[id]) { row.li.remove(); rows.delete(id); } }); Object.values(vm.rows).forEach(task => update(rowOf(task.id), task)); Object.entries(vm.places).forEach(([name, ids]) => arrange(lists[name], ids.map(id => rows.get(id).li))); };}const numbered = count => Array.from({ length: count }, (_, index) => ({ id: String(index + 1), text: `Task ${index + 1}`, done: false }));export const initial = { tasks: [ { id: '1', text: 'Read the thread', done: false }, { id: '2', text: 'Write the task list', done: false }, { id: '3', text: 'Ship it', done: false }, ], nextId: 4, draft: '', refusing: false,};const toggled = (id, task) => (task.id === id ? { ...task, done: !task.done } : task);function added(state) { const text = state.draft.trim(); if (text === '') return state; const task = { id: String(state.nextId), text, done: false }; return { ...state, draft: '', nextId: state.nextId + 1, tasks: [...state.tasks, task] };}export function step(state, [meaning, arg]) { const { tasks, refusing } = state; switch (meaning) { case 'draft': return { ...state, draft: arg }; case 'add': return added(state); case 'toggle': return refusing ? state : { ...state, tasks: tasks.map(task => toggled(arg, task)) }; case 'remove': return { ...state, tasks: tasks.filter(task => task.id !== arg) }; case 'rotate': return { ...state, tasks: [...tasks.slice(1), ...tasks.slice(0, 1)] }; case 'refusing': return { ...state, refusing: arg }; case 'resize': return { ...state, tasks: numbered(arg), nextId: arg + 1 }; default: return state; }}export function refusal(state, [meaning, arg]) { const task = meaning === 'toggle' && state.refusing && state.tasks.find(task => task.id === arg); return task ? `The server refused the change to "${task.text}".` : '';}const ids = tasks => tasks.map(task => task.id);const shown = task => ({ ...task, status: task.done ? 'Done' : 'To do', tone: task.done ? 'success' : 'neutral',});export function view(state) { return { draft: state.draft, refusing: state.refusing, places: { tasks: ids(state.tasks.filter(task => !task.done)), done: ids(state.tasks.filter(task => task.done)), }, rows: Object.fromEntries(state.tasks.map(task => [task.id, shown(task)])), };}export function rowWatcher(root) { let seen = new Set(); return () => { const rows = [...root.querySelectorAll('li')]; const created = rows.filter(row => !seen.has(row)); seen = new Set(rows); return created; };}const isRender = record => record.type === 'state/instance-field-set' && record.field.endsWith('Model');const trace = () => window.BareDOM.traceHistory;export function setCounting(on) { if (on) trace().resume(); else trace().pause();}export function measure(work, counting) { if (counting) trace().clear(); const started = performance.now(); const result = work(); const time = performance.now() - started; return { result, time, renders: counting ? trace().records().filter(isRender).length : null };}const outline = [ { outline: '2px solid var(--flash)', outlineOffset: '2px' }, { outline: '2px solid transparent', outlineOffset: '2px' },];const calm = matchMedia('(prefers-reduced-motion: reduce)');export function flash(rows) { const easing = calm.matches ? 'steps(1, end)' : 'ease-out'; rows.forEach(row => row.animate(outline, { duration: 900, easing }));}const most = 5;function shortened(list) { return list.length <= most ? list : [...list.slice(0, most), `and ${list.length - most} more`];}export function stateText(state) { return JSON.stringify({ ...state, tasks: shortened(state.tasks) }, null, 2);}export function planText(steps) { return JSON.stringify({ remove: shortened(steps.remove), place: shortened(steps.place) }, null, 2);}async function gzipped(bytes) { const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip')); return (await new Response(stream).arrayBuffer()).byteLength;}export async function sizeOf(url) { const bytes = await (await fetch(url)).arrayBuffer(); return { raw: bytes.byteLength, gzip: await gzipped(bytes) };}export const kilobytes = bytes => `${(bytes / 1000).toFixed(1)} kB`;export async function sourceOf(url) { const text = await (await fetch(url)).text(); return { text, lines: text.trimEnd().split('\n').length };}export function sourceBlock(name, { text, lines }, open) { const collapse = document.createElement('x-collapse'); const code = document.createElement('x-code'); collapse.setAttribute('header', `${name}, ${lines} lines`); code.setAttribute('slot', 'content'); code.setAttribute('language', 'javascript'); code.toggleAttribute('line-numbers', true); code.toggleAttribute('expanded', open); collapse.toggleAttribute('open', open); code.setAttribute('code', text); collapse.append(code); return collapse;}import '../../dist/x-trace-history.js';import * as mirror from '../../dist/baremirror-alpha.js';import { init as initAlert } from '../../dist/x-alert.js';import { init as initBadge } from '../../dist/x-badge.js';import { init as initButton } from '../../dist/x-button.js';import { init as initCard } from '../../dist/x-card.js';import { init as initCheckbox } from '../../dist/x-checkbox.js';import { init as initCode } from '../../dist/x-code.js';import { init as initCollapse } from '../../dist/x-collapse.js';import { init as initContainer } from '../../dist/x-container.js';import { init as initFormField } from '../../dist/x-form-field.js';import { init as initGrid } from '../../dist/x-grid.js';import { init as initKbd } from '../../dist/x-kbd.js';import { init as initSelect } from '../../dist/x-select.js';import { init as initSwitch } from '../../dist/x-switch.js';import { init as initTheme } from '../../dist/x-theme.js';import { init as initTypography } from '../../dist/x-typography.js';import { initial, step, view, refusal } from './app.js';import { mount as mountRebuild } from './rebuild.js';import { mount as mountByHand } from './by-hand.js';import { mount as mountFunctions } from './functions.js';import { rowWatcher, measure, setCounting, flash } from './measure.js';import { stateText, planText, sizeOf, kilobytes, sourceOf, sourceBlock } from './boxes.js';[initAlert, initBadge, initButton, initCard, initCheckbox, initCode, initCollapse, initContainer, initFormField, initGrid, initKbd, initSelect, initSwitch, initTheme, initTypography] .forEach(init => init());const leftRenderers = { rebuild: mountRebuild, 'by-hand': mountByHand };const leftFiles = { rebuild: 'rebuild.js', 'by-hand': 'by-hand.js' };const files = ['functions.js', 'rebuild.js', 'by-hand.js', 'app.js', 'measure.js', 'boxes.js', 'main.js'];const otherUserEvery = 2000;const noticeStays = 1500;const manyRows = 100;const forCopy = new URLSearchParams(location.search).has('copy');const byId = id => document.getElementById(id);const draft = byId('draft');const refusing = byId('refusing');const notice = byId('notice');const countingSwitch = byId('counting');const leftChoice = byId('left-renderer');let state = initial;let counting = true;let sources = {};function makeSide(id, mount) { const root = byId(id); return { root, numbers: byId(`${id}-stats`), render: mount(root, dispatch), newRows: rowWatcher(root), rowsMade: 0, renders: 0 };}const left = makeSide('left', mountRebuild);const right = makeSide('right', mountFunctions);const sides = [left, right];function showNumber(side, name, value) { side.numbers.querySelector(`[data-count="${name}"]`).textContent = value;}function report(side, created, { time, renders }) { side.rowsMade += created.length; side.renders += renders ?? 0; showNumber(side, 'shown', side.root.querySelectorAll('li').length); showNumber(side, 'last', created.length); showNumber(side, 'total', side.rowsMade); showNumber(side, 'time', `${time.toFixed(1)} ms`); showNumber(side, 'renders', renders ?? 'not counted'); showNumber(side, 'all-renders', counting ? side.renders : 'not counted'); flash(created);}function startTotals() { sides.forEach(side => { side.rowsMade = 0; side.renders = 0; });}function setCountingRenders(on) { if (on && !counting) sides.forEach(side => { side.renders = 0; }); counting = on; countingSwitch.toggleAttribute('checked', on); setCounting(on);}let noticeTimer = null;function hideNotice() { clearTimeout(noticeTimer); notice.toggleAttribute('hidden', true);}function showNotice(text) { hideNotice(); if (text === '') return; notice.setAttribute('text', text); notice.toggleAttribute('hidden', false); noticeTimer = setTimeout(hideNotice, noticeStays);}function showControls(vm) { byId('page').dataset.rows = state.tasks.length >= manyRows ? 'many' : 'few'; if (draft.getAttribute('value') !== vm.draft) draft.setAttribute('value', vm.draft); refusing.toggleAttribute('checked', vm.refusing); byId('state').setAttribute('code', stateText(state));}function showLines() { const leftSource = sources[leftFiles[leftChoice.getAttribute('value')]]; const rightSource = sources['functions.js']; if (leftSource) byId('left-lines').textContent = `${leftSource.lines} lines of code`; if (rightSource) byId('right-lines').textContent = `${rightSource.lines} lines of code`;}async function showSizes() { const [functions, base] = await Promise.all( [sizeOf('../dist/baremirror-alpha.js'), sizeOf('../dist/base.js')]); byId('size').textContent = `${Object.keys(mirror).length} functions, ${kilobytes(functions.gzip)} compressed. ` + `They hold no state of their own. What they remember is on the node it concerns. ` + `They load beside the file that every BareDOM component needs, ` + `which is ${kilobytes(base.gzip)} compressed.`;}async function showSources() { const loaded = await Promise.all(files.map(name => sourceOf(`mirror/${name}`))); sources = Object.fromEntries(files.map((name, index) => [name, loaded[index]])); byId('sources').replaceChildren(...files.map(name => sourceBlock(name, sources[name], forCopy))); showLines();}function showSide(side, vm) { const measured = measure(() => side.render(vm), counting); report(side, side.newRows(), measured); return measured.result;}function show() { const vm = view(state); showControls(vm); showSide(left, vm); byId('plan').setAttribute('code', planText(showSide(right, vm)));}function keepingScroll(work) { const { scrollX, scrollY } = window; work(); window.scrollTo(scrollX, scrollY);}function dispatch(message) { const next = step(state, message); showNotice(refusal(state, message)); if (next === state) return; state = next; keepingScroll(show);}let otherUserTurn = 0;function otherUser() { const { tasks, refusing: refused } = state; otherUserTurn += 1; const moves = otherUserTurn % 2 === 1 || tasks.length === 0 || refused; dispatch(moves ? ['rotate'] : ['toggle', tasks[0].id]);}let otherUserTimer = null;function setOtherUser(on) { clearInterval(otherUserTimer); otherUserTimer = on ? setInterval(otherUser, otherUserEvery) : null;}function useLeftRenderer(name) { leftChoice.setAttribute('value', name); left.root.querySelectorAll('ul').forEach(list => list.replaceWith(list.cloneNode(false))); left.render = leftRenderers[name](left.root, dispatch); startTotals(); showLines(); show();}function resize(rows) { setCountingRenders(rows < manyRows); dispatch(['resize', rows]);}draft.addEventListener('x-form-field-input', event => dispatch(['draft', event.detail.value]));draft.addEventListener('keydown', event => { if (event.key === 'Enter' && !event.isComposing) dispatch(['add']);});byId('add').addEventListener('press', () => dispatch(['add']));byId('rotate').addEventListener('mousedown', event => event.preventDefault());byId('rotate').addEventListener('press', () => dispatch(['rotate']));byId('rows-few').addEventListener('press', () => resize(10));byId('rows-many').addEventListener('press', () => resize(1000));notice.addEventListener('x-alert-dismiss', event => { event.preventDefault(); hideNotice();});countingSwitch.addEventListener('x-switch-change', event => setCountingRenders(event.detail.checked));refusing.addEventListener('x-switch-change', event => dispatch(['refusing', event.detail.checked]));byId('other-user').addEventListener('x-switch-change', event => setOtherUser(event.detail.checked));byId('preset').addEventListener('select-change', event => { byId('preset').setAttribute('value', event.detail.value); byId('theme').setAttribute('preset', event.detail.value);});leftChoice.addEventListener('select-change', event => useLeftRenderer(event.detail.value));byId('move-before').textContent = 'moveBefore' in Element.prototype ? 'Your browser has it.' : 'Your browser does not have it, so the focus is lost on the right side too.';['state', 'plan'].forEach(id => byId(id).toggleAttribute('expanded', forCopy));show();await Promise.all([showSizes(), showSources()]);document.documentElement.removeAttribute('data-loading');