constform=document.querySelector('.todo-form');constinput=document.querySelector('#todo-input');constlist=document.querySelector('.todo-list');/** @type {string[]} */lettodos=[];form.addEventListener('submit',(event)=>{event.preventDefault();consttext=input.value.trim();if(!text)return;todos.push(text);input.value='';render();});functionrender(){list.innerHTML='';for(consttextoftodos){constli=document.createElement('li');li.textContent=text;list.appendChild(li);}}render();Teaching order: select → state array → submit handler → render loop → initial render.
functionrender(){list.innerHTML='';todos.forEach((text,index)=>{constli=document.createElement('li');li.textContent=text;constremove=document.createElement('button');remove.type='button';remove.textContent='Delete';remove.addEventListener('click',()=>{todos.splice(index,1);render();});li.appendChild(remove);list.appendChild(li);});}Lesson hook: Each delete closes over its index; re-render after splice keeps UI in sync.
Better pattern (teach in deep): store objects with ids, delete by id not index.
lettodos=[];functionaddTodo(text){todos.push({id: crypto.randomUUID(), text });render();}functiondeleteTodo(id){todos=todos.filter((t)=>t.id!==id);render();}Lesson hook:id identifies one todo even when list order changes.
constSTORAGE_KEY='todos';functionload(){constraw=localStorage.getItem(STORAGE_KEY);todos=raw ? JSON.parse(raw) : [];}functionsave(){localStorage.setItem(STORAGE_KEY,JSON.stringify(todos));}functionrender(){// ... build DOM ...save();}Lesson hook:JSON.stringify turns array into string for storage; parse reverses on load.
Hand off: server persistence → devlearn-apis.
asyncfunctionloadTodos(){try{constres=awaitfetch('/api/todos');if(!res.ok)thrownewError(`HTTP ${res.status}`);todos=awaitres.json();render();}catch(err){console.error('Load failed',err);// show user-visible message in DOM}}Lesson hook: Network fails; try/catch prevents silent broken UI.