Learn JavaScript 10 Exercises with Code

Download PDF

Download Source Code

21) DOM Traversal Visualizer

Build: Click any nested element to highlight the target, its ancestors, and its siblings, and show the path to root.
Objectives: closest, parents/children/siblings, DOM introspection.
Steps: Open file → click around the left “tree” → watch highlights and path update.
Code (save as exercise21_traversal.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>DOM Traversal Visualizer</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  .layout{display:grid;grid-template-columns:1fr 1fr;gap:1rem;}

  .tree{border:1px solid #ddd;border-radius:.5rem;padding:1rem;}

  .box{border:1px solid #ccc;border-radius:.5rem;padding:.5rem;margin:.5rem 0;}

  .box .box{margin-left:1rem;}

  .legend span{display:inline-block;margin-right:.5rem;padding:.15rem .35rem;border-radius:.35rem;}

  .target{background:#e6f7ff;border-color:#5ac8fa;}

  .anc{outline:2px solid #ffd666;}

  .sib{background:#f9f9f9;}

  pre{background:#f6f8fa;padding:.75rem;border-radius:.5rem;overflow:auto;max-height:300px;}

  .clickme{cursor:pointer;}

</style>

</head>

<body>

<h1>JS DOM Exercise 21 — Traversal & closest()</h1>

<p>Click any element in the left tree. We will highlight <em>target</em>, its <em>ancestors</em>, and its <em>siblings</em>, and display the path to root.</p>

<div class=”legend”>

  <span style=”background:#e6f7ff;border:1px solid #5ac8fa;”>target</span>

  <span style=”border:1px solid #ffd666;background:#fffbe6;”>ancestor</span>

  <span style=”background:#f9f9f9;border:1px solid #ddd;”>sibling</span>

</div>

<div class=”layout”>

  <div class=”tree” id=”tree”>

    <div class=”box clickme” id=”a”>

      A

      <div class=”box clickme” id=”b”>

        B

        <div class=”box clickme” id=”c”>

          C

          <div class=”box clickme” id=”d”>D</div>

          <div class=”box clickme” id=”e”>E</div>

        </div>

        <div class=”box clickme” id=”f”>F</div>

      </div>

      <div class=”box clickme” id=”g”>

        G

        <div class=”box clickme” id=”h”>H</div>

      </div>

    </div>

  </div>

  <div>

    <h3>Path to root</h3>

    <pre id=”path”></pre>

  </div>

</div>

<script>

const tree = document.getElementById(‘tree’);

const pathEl = document.getElementById(‘path’);

function clearMarks(){

  tree.querySelectorAll(‘.target,.anc,.sib’).forEach(el => {

    el.classList.remove(‘target’,’anc’,’sib’);

  });

}

function describe(el){

  const id = el.id ? ‘#’ + el.id : ”;

  const cls = el.className ? ‘.’ + […el.classList].filter(c=>![‘target’,’anc’,’sib’,’clickme’,’box’].includes(c)).join(‘.’) : ”;

  return el.tagName.toLowerCase() + id + cls;

}

tree.addEventListener(‘click’, (e) => {

  const target = e.target.closest(‘.box’);

  if (!target) return;

  clearMarks();

  target.classList.add(‘target’);

  const parent = target.parentElement.closest(‘.box’);

  if (parent){

    […parent.children].forEach(ch => {

      if (ch !== target && ch.classList.contains(‘box’)) ch.classList.add(‘sib’);

    });

  }

  const path = [];

  let cur = target;

  while (cur && cur !== tree){

    path.push(describe(cur));

    if (cur !== target) cur.classList.add(‘anc’);

    cur = cur.parentElement.closest(‘.box’);

  }

  path.push(‘div#tree’);

  pathEl.textContent = path.join(‘  ←  ‘);

});

</script>

</body>

</html>

Breakdown: We use closest(‘.box’) to snap to a box; walk ancestors and mark .anc; compute siblings from the parent; print a readable CSS-like path.


22) Keyboard Navigation List (j/k, Enter)

Build: Navigate a list with keyboard shortcuts; toggle done.
Objectives: Global key handling, focus management, active item state.
Steps: Press j/k or arrows; Enter toggles done; Home/End jump.
Code (save as exercise22_keyboard_nav.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>Keyboard Navigation (j/k, Enter)</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  ul{padding-left:1.1rem;max-width:520px;}

  li{padding:.35rem .5rem;border-radius:.35rem;cursor:pointer;}

  li.active{background:#e6f7ff;border:1px solid #91d5ff;}

  li.done{text-decoration:line-through;color:#888;}

  .hint{color:#666}

</style>

</head>

<body>

<h1>JS DOM Exercise 22 — Keyboard List</h1>

<p class=”hint”>Use <kbd>j</kbd>/<kbd>k</kbd> to move, <kbd>Enter</kbd> to toggle done, <kbd>Home</kbd>/<kbd>End</kbd> to jump.</p>

<ul id=”list”>

  <li data-id=”1″>Learn querySelector</li>

  <li data-id=”2″>Practice event delegation</li>

  <li data-id=”3″>Master classList</li>

  <li data-id=”4″>Play with localStorage</li>

  <li data-id=”5″>Build a small app</li>

</ul>

<script>

const items = […document.querySelectorAll(‘#list li’)];

let index = 0;

function setActive(i){

  items.forEach(li => li.classList.remove(‘active’));

  index = (i + items.length) % items.length;

  items[index].classList.add(‘active’);

  items[index].scrollIntoView({block:’nearest’});

}

setActive(0);

document.addEventListener(‘keydown’, (e) => {

  if (e.key === ‘j’ || e.key === ‘ArrowDown’) setActive(index+1);

  else if (e.key === ‘k’ || e.key === ‘ArrowUp’) setActive(index-1);

  else if (e.key === ‘Home’) setActive(0);

  else if (e.key === ‘End’) setActive(items.length-1);

  else if (e.key === ‘Enter’){

    items[index].classList.toggle(‘done’);

    console.log(‘Toggled item’, items[index].dataset.id);

  }

});

</script>

</body>

</html>

Breakdown: Maintain an index; setActive updates classes and scroll; keydown maps keys to actions.


23) Inline Editable Table with Validation

Build: Click cells to edit; Enter saves, Esc cancels; age validated 1–120.
Objectives: In-place editors, dataset, cancel/commit flows.
Steps: Click a cell → edit → confirm/cancel; invalid ages turn red.
Code (save as exercise23_inline_edit_table.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>Inline Editable Table</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  table{border-collapse:collapse;min-width:520px;}

  th,td{border:1px solid #ddd;padding:.5rem .75rem;}

  td.editing{background:#fffbe6;}

  td.invalid{background:#ffd6d6;}

  .note{color:#666;margin:.5rem 0;}

</style>

</head>

<body>

<h1>JS DOM Exercise 23 — Inline Editing</h1>

<p class=”note”>Click a cell to edit. <kbd>Enter</kbd> to save, <kbd>Esc</kbd> to cancel. Age must be 1–120.</p>

<table>

  <thead><tr><th>Name</th><th>Age</th><th>City</th></tr></thead>

  <tbody id=”tb”>

    <tr><td>Ava</td><td>31</td><td>Toronto</td></tr>

    <tr><td>Noah</td><td>26</td><td>Ottawa</td></tr>

    <tr><td>Mia</td><td>22</td><td>Calgary</td></tr>

  </tbody>

</table>

<script>

const tb = document.getElementById(‘tb’);

let editing = null;

tb.addEventListener(‘click’, (e) => {

  const td = e.target.closest(‘td’);

  if(!td || td === editing) return;

  startEdit(td);

});

function startEdit(td){

  cancelEdit();

  editing = td;

  const old = td.textContent;

  td.dataset.old = old;

  td.classList.add(‘editing’);

  const input = document.createElement(‘input’);

  input.value = old;

  input.style.width = ‘100%’;

  td.textContent = ”;

  td.appendChild(input);

  input.focus();

  input.select();

  input.addEventListener(‘keydown’, (e) => {

    if (e.key === ‘Enter’) commit(td, input.value.trim());

    else if (e.key === ‘Escape’) cancelEdit();

  });

  input.addEventListener(‘blur’, () => commit(td, input.value.trim()));

}

function isValid(td, value){

  const index = […td.parentElement.children].indexOf(td);

  if (index === 1){

    const n = Number(value);

    return Number.isInteger(n) && n >= 1 && n <= 120;

  }

  return value.length > 0;

}

function commit(td, value){

  if(!isValid(td, value)){

    td.classList.add(‘invalid’);

    return;

  }

  td.classList.remove(‘invalid’);

  td.textContent = value;

  td.classList.remove(‘editing’);

  console.log(‘Changed cell’, td.dataset.old, ‘→’, value);

  editing = null;

}

function cancelEdit(){

  if(!editing) return;

  const td = editing;

  td.classList.remove(‘invalid’,’editing’);

  td.textContent = td.dataset.old;

  editing = null;

}

</script>

</body>

</html>

Breakdown: Replace a cell with an <input> while editing; use column index to validate age; support commit/cancel.


24) Virtualized List (10,000 rows)

Build: Efficiently render only the visible slice of a huge list.
Objectives: Windowing, scrollTop, translateY, avoiding reflow.
Steps: Scroll to see rows update smoothly.
Code (save as exercise24_virtualized_list.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>Virtualized List (10,000 items) — Fixed Demo</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  .controls{display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;flex-wrap:wrap}

  .viewport{height:320px;border:2px solid #bbb;border-radius:.5rem;overflow:auto;position:relative;background:#fafafa}

  .spacer{height:0;}

  .inner{position:absolute;left:0;right:0;will-change:transform;}

  .row{height:24px; line-height:24px; padding:0 .5rem; border-bottom:1px solid #eee; background:#fff;}

  .row:nth-child(even){background:#fdfdfd;}

  .hint{color:#666;margin:.25rem 0 .75rem}

  .stat{font-family:ui-monospace, SFMono-Regular, Menlo, monospace;}

  button{padding:.4rem .6rem}

</style>

</head>

<body>

  <h1>Virtualized List (10,000 items)</h1>

  <p class=”hint”>Scroll <strong>inside the bordered box</strong> below. Or use the jump buttons to move to a specific row.</p>

  <div class=”controls”>

    <button data-jump=”0″>Jump to 0</button>

    <button data-jump=”5000″>Jump to 5,000</button>

    <button data-jump=”9999″>Jump to 9,999</button>

    <span class=”stat” id=”range”></span>

  </div>

  <div class=”viewport” id=”vp” aria-label=”Virtualized list viewport”>

    <div class=”spacer” id=”spacer”></div>

    <div class=”inner” id=”inner”></div>

  </div>

<script>

const total = 10000;     // total rows

const itemHeight = 24;   // px per row (match .row height + borders)

const buffer = 6;        // extra rows above/below for smoother scrolling

const vp = document.getElementById(‘vp’);

const inner = document.getElementById(‘inner’);

const spacer = document.getElementById(‘spacer’);

const rangeLabel = document.getElementById(‘range’);

// 1) make the container scrollable as if it had 10k items

spacer.style.height = (total * itemHeight) + ‘px’;

// 2) render only what’s visible + buffer

function render(){

  const scrollTop = vp.scrollTop;

  const height = vp.clientHeight;

  // figure out the slice of rows we need

  const start = Math.max(0, Math.floor(scrollTop / itemHeight) – buffer);

  const end = Math.min(total, Math.ceil((scrollTop + height) / itemHeight) + buffer);

  // position the slice at the right height

  inner.style.transform = `translateY(${start * itemHeight}px)`;

  // paint the slice

  inner.innerHTML = ”;

  for(let i=start; i<end; i++){

    const div = document.createElement(‘div’);

    div.className = ‘row’;

    div.textContent = ‘Row #’ + i;

    inner.appendChild(div);

  }

  // update status

  rangeLabel.textContent = `Visible rows ~ ${start}…${end-1} (rendered: ${end – start} / ${total})`;

}

// 3) handle scrolling

vp.addEventListener(‘scroll’, render);

// 4) initial paint

render();

// 5) jump helpers

document.querySelectorAll(‘button[data-jump]’).forEach(btn => {

  btn.addEventListener(‘click’, () => {

    const i = Number(btn.getAttribute(‘data-jump’));

    // scroll the viewport so that row i is at the top

    vp.scrollTop = i * itemHeight;

    render();

  });

});

</script>

</body>

</html>

Breakdown: A big spacer sets total height; we translate the inner slice and render only what’s visible.


25) Filter Chips + Search

Build: Filter cards by category chips and free-text search.
Objectives: dataset, stateful filters, rendering pipelines.
Steps: Toggle chips (All/Input/Audio/Display); type to filter by name.
Code (save as exercise25_filter_chips.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>Filter Chips by Category + Search</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  .chips{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:.5rem;}

  .chip{padding:.35rem .6rem;border:1px solid #ccc;border-radius:999px;cursor:pointer;}

  .chip.active{background:#e6f7ff;border-color:#91d5ff;}

  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem;}

  .card{border:1px solid #ddd;border-radius:.5rem;padding:.6rem;}

  input{padding:.4rem .5rem;max-width:360px;width:100%;}

</style>

</head>

<body>

<h1>JS DOM Exercise 25 — Filter Chips</h1>

<div class=”chips” id=”chips”>

  <span class=”chip active” data-cat=”all”>All</span>

  <span class=”chip” data-cat=”input”>Input</span>

  <span class=”chip” data-cat=”audio”>Audio</span>

  <span class=”chip” data-cat=”display”>Display</span>

</div>

<input id=”search” placeholder=”Search by name…” autocomplete=”off”>

<div class=”grid” id=”grid”></div>

<script>

const data = [

  {name:’Text Field’, cat:’input’}, {name:’Checkbox’, cat:’input’},

  {name:’Radio Group’, cat:’input’}, {name:’Speaker’, cat:’audio’},

  {name:’Headphones’, cat:’audio’}, {name:’Monitor’, cat:’display’},

  {name:’Projector’, cat:’display’}, {name:’OLED TV’, cat:’display’}

];

const grid = document.getElementById(‘grid’);

const chips = document.getElementById(‘chips’);

const search = document.getElementById(‘search’);

let active = ‘all’;

function render(){

  grid.innerHTML = ”;

  const q = search.value.trim().toLowerCase();

  data.filter(item => (active===’all’||item.cat===active) && (!q || item.name.toLowerCase().includes(q)))

      .forEach(item => {

        const d = document.createElement(‘div’);

        d.className=’card’;

        d.innerHTML = `<strong>${item.name}</strong><div>Category: ${item.cat}</div>`;

        grid.appendChild(d);

      });

}

render();

chips.addEventListener(‘click’, (e) => {

  const chip = e.target.closest(‘.chip’);

  if(!chip) return;

  chips.querySelectorAll(‘.chip’).forEach(c => c.classList.remove(‘active’));

  chip.classList.add(‘active’);

  active = chip.dataset.cat;

  render();

});

search.addEventListener(‘input’, render);

</script>

</body>

</html>

Breakdown: Maintain active category state; filter pipeline combines category + query; render cards.


26) FormData Builder with JSON & Query Preview

Build: Add key/value rows, then preview as JSON and query string.
Objectives: FormData, serializing, table→data mapping.
Steps: Add pairs → Preview → see JSON and a=b&c=d.
Code (save as exercise26_formdata_builder.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>FormData Builder + JSON/Query Preview</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  .row{display:flex;gap:.5rem;margin:.5rem 0;}

  input{padding:.4rem;}

  button{padding:.4rem .6rem;}

  pre{background:#f6f8fa;padding:.75rem;border-radius:.5rem;overflow:auto;}

  table{border-collapse:collapse;min-width:420px;margin-top:.5rem;}

  th,td{border:1px solid #ddd;padding:.35rem .5rem;}

</style>

</head>

<body>

<h1>JS DOM Exercise 26 — Build FormData</h1>

<div class=”row”>

  <input id=”key” placeholder=”Key”>

  <input id=”val” placeholder=”Value”>

  <button id=”add”>Add Pair</button>

  <button id=”clear”>Clear</button>

</div>

<table>

  <thead><tr><th>Key</th><th>Value</th></tr></thead>

  <tbody id=”tbody”></tbody>

</table>

<div class=”row”>

  <button id=”preview”>Preview JSON & Query</button>

</div>

<h3>JSON</h3>

<pre id=”json”></pre>

<h3>Query string</h3>

<pre id=”qs”></pre>

<script>

const tb = document.getElementById(‘tbody’);

document.getElementById(‘add’).addEventListener(‘click’, () => {

  const k = document.getElementById(‘key’).value.trim();

  const v = document.getElementById(‘val’).value;

  if(!k) return;

  const tr = document.createElement(‘tr’);

  tr.innerHTML = `<td>${k}</td><td>${v}</td>`;

  tb.appendChild(tr);

  document.getElementById(‘key’).value=”; document.getElementById(‘val’).value=”;

});

document.getElementById(‘clear’).addEventListener(‘click’, () => tb.innerHTML=”);

document.getElementById(‘preview’).addEventListener(‘click’, () => {

  const fd = new FormData();

  […tb.querySelectorAll(‘tr’)].forEach(tr => {

    const [k,v] = […tr.children].map(td => td.textContent);

    fd.append(k, v);

  });

  const obj = {};

  fd.forEach((v,k)=>{

    if (obj[k] !== undefined){

      obj[k] = Array.isArray(obj[k]) ? […obj[k], v] : [obj[k], v];

    } else obj[k] = v;

  });

  document.getElementById(‘json’).textContent = JSON.stringify(obj,null,2);

  const qs = […fd.entries()].map(([k,v]) => encodeURIComponent(k)+’=’+encodeURIComponent(v)).join(‘&’);

  document.getElementById(‘qs’).textContent = qs;

});

</script>

</body>

</html>

Breakdown: Table rows → FormData → object & query; repeated keys become arrays.


27) Drag-to-Select (Marquee) on a Grid

Build: Click-drag a selection rectangle to select multiple cards.
Objectives: Pointer math, bounding-box intersection, class toggling.
Steps: Mouse down, drag across cards, release; clear selection via button.
Code (save as exercise27_drag_select.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>Drag-to-Select Cards (Marquee)</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;user-select:none;}

  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.6rem;position:relative;}

  .card{border:1px solid #ddd;border-radius:.5rem;padding:1rem;text-align:center;background:#fff;}

  .card.selected{outline:3px solid #5ac8fa;}

  .marquee{position:fixed;border:2px dashed #91d5ff;background:rgba(230,247,255,.3);pointer-events:none;display:none;}

  .actions{margin:.5rem 0;}

</style>

</head>

<body>

<h1>JS DOM Exercise 27 — Marquee Selection</h1>

<div class=”actions”><button id=”clear”>Clear Selection</button></div>

<div class=”grid” id=”grid”></div>

<div class=”marquee” id=”marquee”></div>

<script>

const grid = document.getElementById(‘grid’);

const marquee = document.getElementById(‘marquee’);

// Create 48 cards

for (let i=1;i<=48;i++){

  const d = document.createElement(‘div’);

  d.className=’card’; d.textContent=’Card ‘+i;

  grid.appendChild(d);

}

let start = null;

function rectFromPoints(a,b){

  const x = Math.min(a.x,b.x), y = Math.min(a.y,b.y);

  const w = Math.abs(a.x-b.x), h = Math.abs(a.y-b.y);

  return {x,y,w,h};

}

document.addEventListener(‘mousedown’, (e)=>{

  start = {x:e.clientX,y:e.clientY};

  marquee.style.display=’block’;

  marquee.style.left=start.x+’px’; marquee.style.top=start.y+’px’;

  marquee.style.width=’0px’; marquee.style.height=’0px’;

});

document.addEventListener(‘mousemove’, (e)=>{

  if(!start) return;

  const r = rectFromPoints(start, {x:e.clientX,y:e.clientY});

  marquee.style.left=r.x+’px’; marquee.style.top=r.y+’px’;

  marquee.style.width=r.w+’px’; marquee.style.height=r.h+’px’;

  const mrect = marquee.getBoundingClientRect();

  document.querySelectorAll(‘.card’).forEach(card => {

    const crect = card.getBoundingClientRect();

    const overlap = !(mrect.right < crect.left || mrect.left > crect.right || mrect.bottom < crect.top || mrect.top > crect.bottom);

    card.classList.toggle(‘selected’, overlap);

  });

});

document.addEventListener(‘mouseup’, ()=>{

  start = null; marquee.style.display=’none’;

});

document.getElementById(‘clear’).addEventListener(‘click’, () => {

  document.querySelectorAll(‘.card’).forEach(c=>c.classList.remove(‘selected’));

});

</script>

</body>

</html>

Breakdown: We draw a fixed-position rectangle and check overlap with each card’s bounding box as you drag.


28) CSS Variables Playground

Build: Sliders control hue, radius, and padding using CSS custom properties.
Objectives: Update CSS variables via JS, live styling.
Steps: Drag sliders and watch the card and button restyle live.
Code (save as exercise28_css_vars_playground.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>CSS Variables Playground</title>

<style>

  :root{

    –hue: 210;

    –radius: 12px;

    –pad: 12px;

  }

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  .controls{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;margin-bottom:1rem;}

  .card{border:1px solid hsl(var(–hue) 30% 75%);border-radius:var(–radius);padding:var(–pad);background:hsl(var(–hue) 50% 97%);}

  .btn{padding:.5rem .8rem;border-radius:calc(var(–radius) / 2);border:1px solid hsl(var(–hue) 30% 55%);background:hsl(var(–hue) 80% 95%);}

  label{display:block;font-size:.9rem;color:#444}

  input[type=range]{width:100%;}

</style>

</head>

<body>

<h1>JS DOM Exercise 28 — CSS Vars</h1>

<div class=”controls”>

  <div><label>Hue <span id=”hval”>210</span></label><input id=”hue” type=”range” min=”0″ max=”360″ value=”210″></div>

  <div><label>Radius <span id=”rval”>12</span>px</label><input id=”radius” type=”range” min=”0″ max=”32″ value=”12″></div>

  <div><label>Padding <span id=”pval”>12</span>px</label><input id=”pad” type=”range” min=”0″ max=”48″ value=”12″></div>

</div>

<div class=”card”>

  <h3>Preview Card</h3>

  <p>This card’s border, background, radius, and padding react to the sliders above.</p>

  <button class=”btn”>Button</button>

</div>

<script>

const root = document.documentElement.style;

const hue = document.getElementById(‘hue’);

const radius = document.getElementById(‘radius’);

const pad = document.getElementById(‘pad’);

function sync(){

  root.setProperty(‘–hue’, hue.value);

  root.setProperty(‘–radius’, radius.value + ‘px’);

  root.setProperty(‘–pad’, pad.value + ‘px’);

  document.getElementById(‘hval’).textContent = hue.value;

  document.getElementById(‘rval’).textContent = radius.value;

  document.getElementById(‘pval’).textContent = pad.value;

}

[hue, radius, pad].forEach(i => i.addEventListener(‘input’, sync));

sync();

</script>

</body>

</html>

Breakdown: Tie range inputs to CSS variables on :root for live theming.


29) ResizeObserver + Resizable Sidebar

Build: Drag a handle to resize a sidebar; width readout updates via ResizeObserver.
Objectives: Pointer tracking, CSS bounds, ResizeObserver.
Steps: Drag the sidebar edge; watch the width display update.
Code (save as exercise29_resize_observer.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>ResizeObserver + Resizable Sidebar</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;margin:0;}

  .app{display:flex;min-height:100vh;}

  .sidebar{width:280px;min-width:160px;max-width:600px;border-right:1px solid #ddd;position:relative;background:#fafafa;}

  .handle{position:absolute;right:-4px;top:0;width:8px;height:100%;cursor:col-resize;}

  .main{flex:1;padding:1rem;}

  .meta{padding:.5rem 1rem;border-bottom:1px solid #eee;background:#fff;}

</style>

</head>

<body>

<div class=”meta”>Sidebar width: <strong id=”w”>—</strong> px</div>

<div class=”app”>

  <aside class=”sidebar” id=”sb”>

    <div class=”handle” id=”h”></div>

    <div style=”padding:1rem;”>

      <h3>Sidebar</h3>

      <p>Drag the right edge to resize.</p>

    </div>

  </aside>

  <main class=”main”>

    <h1>JS DOM Exercise 29 — ResizeObserver</h1>

    <p>The width above updates live as you drag.</p>

  </main>

</div>

<script>

const sb = document.getElementById(‘sb’);

const h = document.getElementById(‘h’);

const out = document.getElementById(‘w’);

let startX, startW;

h.addEventListener(‘mousedown’, (e)=>{

  startX = e.clientX; startW = sb.offsetWidth;

  document.addEventListener(‘mousemove’, onMove);

  document.addEventListener(‘mouseup’, onUp, {once:true});

});

function onMove(e){

  const dx = e.clientX – startX;

  let w = Math.min(600, Math.max(160, startW + dx));

  sb.style.width = w + ‘px’;

}

function onUp(){ document.removeEventListener(‘mousemove’, onMove); }

const ro = new ResizeObserver(entries => {

  for (const entry of entries){

    out.textContent = Math.round(entry.contentRect.width);

  }

});

ro.observe(sb);

</script>

</body>

</html>

Breakdown: Observe the sidebar element’s content box; drag logic clamps width between min/max.


30) Custom Context Menu (Rename/Delete)

Build: Right-click a list item to open a custom menu at cursor; rename/delete actions.
Objectives: contextmenu event, absolute positioning, outside-click/escape close.
Steps: Right-click items → choose rename/delete → click elsewhere to close.
Code (save as exercise30_context_menu.html)

<!doctype html>

<html lang=”en”>

<head>

<meta charset=”utf-8″>

<title>Custom Context Menu</title>

<style>

  body{font:16px/1.5 system-ui,sans-serif;padding:1.5rem;}

  li{padding:.35rem .5rem;border:1px solid #eee;border-radius:.35rem;margin:.25rem 0;}

  .menu{position:fixed;background:#fff;border:1px solid #ddd;border-radius:.5rem;box-shadow:0 6px 16px rgba(0,0,0,.08);display:none;min-width:160px;z-index:10;}

  .menu button{display:block;width:100%;text-align:left;padding:.5rem .75rem;border:0;background:none;cursor:pointer;}

  .menu button:hover{background:#f5f5f5;}

</style>

</head>

<body>

<h1>JS DOM Exercise 30 — Context Menu</h1>

<ul id=”list”>

  <li>Document A</li>

  <li>Document B</li>

  <li>Document C</li>

  <li>Document D</li>

</ul>

<div class=”menu” id=”menu” role=”menu”>

  <button data-action=”rename”>Rename</button>

  <button data-action=”delete”>Delete</button>

</div>

<script>

const list = document.getElementById(‘list’);

const menu = document.getElementById(‘menu’);

let targetItem = null;

list.addEventListener(‘contextmenu’, (e) => {

  const li = e.target.closest(‘li’); if(!li) return;

  e.preventDefault();

  targetItem = li;

  menu.style.left = e.clientX + ‘px’;

  menu.style.top = e.clientY + ‘px’;

  menu.style.display = ‘block’;

});

document.addEventListener(‘click’, (e) => {

  if (!menu.contains(e.target)) menu.style.display = ‘none’;

});

document.addEventListener(‘keydown’, (e) => {

  if (e.key === ‘Escape’) menu.style.display=’none’;

});

menu.addEventListener(‘click’, (e) => {

  const btn = e.target.closest(‘button’); if(!btn) return;

  const action = btn.dataset.action;

  if (action === ‘rename’ && targetItem){

    const name = prompt(‘New name:’, targetItem.textContent);

    if (name) targetItem.textContent = name;

  } else if (action === ‘delete’ && targetItem){

    targetItem.remove();

  }

  menu.style.display = ‘none’;

});

</script>

</body>

</html>

Breakdown: Suppress the default menu; position our menu at clientX/Y; close on click outside or Esc.