carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1// Progressive enhancement for the server-rendered TodoMVC.
2//
3// Every action is already a working HTML form. This script sends those same
4// forms with fetch, asks for the re-rendered page instead of a redirect, and
5// swaps in the new list and footer. It adds TodoMVC's own gestures on top:
6// click a checkbox to toggle, double-click a label to edit, Enter or blur to
7// save, Escape to cancel.
8
9const app = document.querySelector('.todoapp');
10
11// Requests go out one at a time, so the page always shows the last answer.
12let queue = Promise.resolve();
13
14function send(form) {
15 const body = new URLSearchParams(new FormData(form));
16 queue = queue.then(async () => {
17 const res = await fetch(form.action, {
18 method: 'POST',
19 headers: { 'X-Requested-With': 'fetch' },
20 body,
21 });
22 if (!res.ok) throw new Error(`${res.status} from ${form.action}`);
23 apply(new DOMParser().parseFromString(await res.text(), 'text/html'));
24 }).catch((err) => {
25 console.error(err);
26 location.reload();
27 });
28}
29
30// Swaps in the server's fresh list and footer. An editor that's open when
31// the answer arrives stays open with its unsaved text: its item is kept in
32// place (never detached, so it keeps focus) and only refreshed around it.
33function apply(fresh) {
34 app.querySelector('.footer').replaceWith(fresh.querySelector('.footer'));
35 const editing = app.querySelector('.todo-list li.editing');
36 const update = editing && fresh.querySelector(`.todo-list li[data-id="${editing.dataset.id}"]`);
37 if (!update) {
38 app.querySelector('.main').replaceWith(fresh.querySelector('.main'));
39 return;
40 }
41
42 const main = app.querySelector('.main');
43 const freshMain = fresh.querySelector('.main');
44 main.hidden = freshMain.hidden;
45 main.querySelector('form').replaceWith(freshMain.querySelector('form'));
46
47 for (const item of [...main.querySelectorAll('.todo-list > li')]) {
48 if (item !== editing) item.remove();
49 }
50 let before = true;
51 for (const item of [...freshMain.querySelectorAll('.todo-list > li')]) {
52 if (item === update) {
53 before = false;
54 } else if (before) {
55 editing.before(item);
56 } else {
57 editing.parentElement.append(item);
58 }
59 }
60
61 // Bring the kept item's view up to date, leaving the editor's text alone.
62 editing.className = `${update.className} editing`;
63 editing.querySelector('.view').replaceWith(update.querySelector('.view'));
64 editing.querySelector('.edit').defaultValue = update.querySelector('.edit').defaultValue;
65}
66
67app.addEventListener('submit', (event) => {
68 event.preventDefault();
69 const form = event.target;
70 const editing = form.closest('li.editing');
71 if (editing) editing.classList.remove('editing');
72 send(form);
73 if (form.closest('.header')) form.querySelector('.new-todo').value = '';
74});
75
76app.addEventListener('change', (event) => {
77 const box = event.target;
78 if (box.matches('.toggle')) {
79 send(box.closest('li').querySelector('form[action$="/toggle"]'));
80 } else if (box.matches('.toggle-all')) {
81 send(box.form);
82 }
83});
84
85app.addEventListener('dblclick', (event) => {
86 const label = event.target.closest('.todo-list label');
87 if (!label) return;
88 const item = label.closest('li');
89 const input = item.querySelector('.edit');
90 item.classList.add('editing');
91 input.focus();
92 input.setSelectionRange(input.value.length, input.value.length);
93});
94
95app.addEventListener('keydown', (event) => {
96 if (event.key !== 'Escape' || !event.target.matches('.edit')) return;
97 const input = event.target;
98 input.value = input.defaultValue;
99 input.closest('li').classList.remove('editing');
100});
101
102// Leaving the editor saves it, unless Enter or Escape already closed it.
103app.addEventListener('focusout', (event) => {
104 const input = event.target;
105 if (input.matches('.edit') && input.closest('li.editing')) input.form.requestSubmit();
106});
107