// TodoMVC behaviour on top of the server-rendered page. Every change is // still one of the page's own form POSTs; this script sends it with // fetch and an `X-Fragment: 1` header, and the server answers with the // fresh section.main and footer.footer, which replace the old ones. // Without this script the same forms submit normally. const app = document.querySelector('.todoapp'); const header = app.querySelector('.header'); let queue = Promise.resolve(); function render(html) { for (const el of app.querySelectorAll(':scope > .main, :scope > .footer')) el.remove(); header.insertAdjacentHTML('afterend', html); } // POSTs run one after another, in the order they were made. function send(form) { const body = new URLSearchParams(new FormData(form)); queue = queue.then(async () => { try { const res = await fetch(form.action, { method: 'POST', body, headers: { 'X-Fragment': '1' }, credentials: 'same-origin', }); if (!res.ok) throw new Error(`${res.status}`); render(await res.text()); } catch { location.reload(); } }); return queue; } app.addEventListener('submit', (e) => { const form = e.target; e.preventDefault(); if (form.classList.contains('edit-form')) { const li = form.closest('li'); if (!li.classList.contains('editing')) return; li.classList.remove('editing'); } send(form); const input = form.querySelector('.new-todo'); if (input) input.value = ''; }); // The checkboxes belong to their item's toggle form (and the toggle-all // form) through the `form` attribute; a change submits that form. app.addEventListener('change', (e) => { const box = e.target; if (box.matches('.toggle, .toggle-all') && box.form) box.form.requestSubmit(); }); app.addEventListener('dblclick', (e) => { const label = e.target.closest('.todo-list li label'); if (!label) return; const li = label.closest('li'); const input = li.querySelector('.edit'); input.value = label.textContent; li.classList.add('editing'); input.focus(); input.setSelectionRange(input.value.length, input.value.length); }); app.addEventListener('keydown', (e) => { if (e.key !== 'Escape' || !e.target.matches('.edit')) return; const li = e.target.closest('li'); e.target.value = li.querySelector('label').textContent; li.classList.remove('editing'); }); // Leaving the editor saves, as in TodoMVC. focusout bubbles; blur doesn't. app.addEventListener('focusout', (e) => { if (!e.target.matches('.edit')) return; const li = e.target.closest('li'); if (li && li.classList.contains('editing')) e.target.form.requestSubmit(); });