# JavaScript DOM & Events Lab

20 guided workshops. Run each example after its fixture markup.

## 1. Select and change text

The DOM is the browser’s object representation of a document. querySelector returns the first match, or null. textContent changes text without interpreting it as HTML. Select after the target exists; this lab runs your code after its fixture markup.

**Mission:** Change #message to "Hello, DOM!" without replacing the paragraph.

```html
<p id="message">Waiting</p>
```

```javascript
document.querySelector('#message').textContent = 'Hello, DOM!';
```

Hint: Select by ID and assign textContent.

## 2. Update several elements

querySelectorAll returns a static NodeList. Use forEach to visit its elements. Later DOM additions do not automatically appear in that earlier list. Select just the intended elements instead of overwriting the whole document.

**Mission:** Set every .status to "Ready". Leave #other unchanged.

```html
<p class="status">Pending</p><p class="status">Pending</p><p id="other">Keep me</p>
```

```javascript
document.querySelectorAll('.status').forEach(node => node.textContent = 'Ready');
```

Hint: Loop over the selected NodeList.

## 3. Listen for a click

addEventListener registers a callback without calling it immediately. Native buttons support keyboard activation as well as pointer clicks. Keep the initial state until the user acts.

**Mission:** Clicking #go changes #message to "Clicked".

```html
<button id="go">Try me</button><p id="message" role="status">Waiting</p>
```

```javascript
document.querySelector('#go').addEventListener('click', () => {
  document.querySelector('#message').textContent = 'Clicked';
});
```

Hint: Pass a function to addEventListener; do not call it during setup.

## 4. Keep a counter in state

A variable can represent application state; a render step makes that state visible. Reading numbers from formatted UI text is fragile. Increment state, then update the output.

**Mission:** Each click on #add increases #count by one, starting at zero.

```html
<button id="add">Add one</button><output id="count" aria-live="polite">0</output>
```

```javascript
let count = 0;
document.querySelector('#add').addEventListener('click', () => {
  count++;
document.querySelector('#count').textContent = count;
});
```

Hint: Store count outside the callback.

## 5. Toggle a class

classList lets you change a named style state while preserving unrelated classes. toggle adds a missing class and removes an existing one. Avoid assigning className when you only intend to change one class.

**Mission:** Click #toggle to toggle highlighted on #card; retain its card class.

```html
<button id="toggle">Highlight</button><p id="card" class="card">A useful detail</p>
```

```javascript
document.querySelector('#toggle').addEventListener('click', () => document.querySelector('#card').classList.toggle('highlighted'));
```

Hint: Use classList.toggle inside the click handler.

## 6. Read input as it changes

The input event fires for edits as they happen, unlike change which often waits for a committed edit. An input’s current text is in value. Programmatic assignments do not automatically dispatch input events.

**Mission:** Mirror #name into #greeting as "Hello, NAME"; use "Hello, friend" for empty input.

```html
<label>Name <input id="name"></label><p id="greeting" role="status">Hello, friend</p>
```

```javascript
document.querySelector('#name').addEventListener('input', event => {
  document.querySelector('#greeting').textContent = 'Hello, ' + (event.target.value || 'friend');
});
```

Hint: Read event.target.value.

## 7. Handle form submission

Listen on the form so both clicking submit and pressing Enter can follow the same flow. preventDefault stops the native submission; it does not stop propagation. The sandbox blocks real form navigation.

**Mission:** On submit, prevent navigation and show trimmed #email in #result.

```html
<form id="form"><label>Email <input id="email" type="text"></label><button>Save</button></form><p id="result" role="status"></p>
```

```javascript
document.querySelector('#form').addEventListener('submit', event => {
  event.preventDefault();
document.querySelector('#result').textContent = document.querySelector('#email').value.trim();
});
```

Hint: Call event.preventDefault() in the submit handler.

## 8. Validate and explain

Validation should tell a learner what to fix. Do not rely on color alone. Keep the user’s input and show a helpful message. Client validation improves usability; servers still need to validate submitted data.

**Mission:** On input, show "At least 3 characters" for trimmed names shorter than 3; otherwise "Looks good".

```html
<label>Display name <input id="name" aria-describedby="help"></label><p id="help" role="status"></p>
```

```javascript
document.querySelector('#name').addEventListener('input', e => {
  document.querySelector('#help').textContent = e.target.value.trim().length < 3 ? 'At least 3 characters' : 'Looks good';
});
```

Hint: Trim before measuring length.

## 9. Create an element safely

createElement constructs a node; append attaches it. Use textContent when inserting user-provided text so markup is displayed literally. Rebuilding a container with innerHTML can discard attached listeners and introduce injection risks.

**Mission:** Click #add to append an li containing #item’s value literally.

```html
<label>Item <input id="item"></label><button id="add">Add item</button><ul id="list"></ul>
```

```javascript
document.querySelector('#add').addEventListener('click', () => {
  const li = document.createElement('li');
li.textContent = document.querySelector('#item').value;
document.querySelector('#list').append(li);
});
```

Hint: Set the new li’s textContent before appending.

## 10. Remove a chosen item

closest finds the nearest matching ancestor, including the element itself. This is useful when the click lands on text or an icon nested in a button. remove detaches the matched node from the document.

**Mission:** Each remove button deletes only its containing li.

```html
<ul id="list"><li>Alpha <button class="remove">Remove Alpha</button></li><li>Beta <button class="remove">Remove Beta</button></li></ul>
```

```javascript
document.querySelectorAll('.remove').forEach(button => button.addEventListener('click', () => button.closest('li').remove()));
```

Hint: Use closest("li") from the clicked button.

## 11. Delegate events for dynamic content

Most clicks bubble to ancestors. A parent listener can handle descendants added later. Check the matched action and ensure it belongs to the intended container. event.target is the original target; currentTarget is the listener owner.

**Mission:** Use one #list listener so clicking any .pick button, including later additions, shows its data-name in #result.

```html
<div id="list"><button class="pick" data-name="Alpha"><span>Choose Alpha</span></button></div><p id="result" role="status"></p>
```

```javascript
document.querySelector('#list').addEventListener('click', e => {
  const button = e.target.closest('.pick');
if (button && e.currentTarget.contains(button)) document.querySelector('#result').textContent = button.dataset.name;
});
```

Hint: Use event.target.closest(".pick") in the parent handler.

## 12. Synchronize expanded state

A disclosure has both visible state and an accessible state. Update hidden on its panel and aria-expanded on its button together. aria-controls identifies the controlled element; a native button handles keyboard activation.

**Mission:** Toggle #panel visibility and #toggle aria-expanded together.

```html
<button id="toggle" aria-controls="panel" aria-expanded="false">Details</button><div id="panel" hidden>More detail</div>
```

```javascript
document.querySelector('#toggle').addEventListener('click', e => {
  const panel=document.querySelector('#panel');
panel.hidden=!panel.hidden;
e.currentTarget.setAttribute('aria-expanded', String(!panel.hidden));
});
```

Hint: The expanded state is the inverse of panel.hidden.

## 13. Filter a visible list

Use a normalized query for case-insensitive comparisons. hidden can hide unmatched items without deleting their data. Empty searches should normally restore all results. On larger lists, keep the dataset separate from its rendering.

**Mission:** Filter #list li by #search text, ignoring case and surrounding query spaces.

```html
<label>Search <input id="search" type="search"></label><ul id="list"><li>JavaScript</li><li>Python</li><li>Java</li></ul>
```

```javascript
document.querySelector('#search').addEventListener('input', e => {
  const query=e.target.value.trim().toLowerCase();
document.querySelectorAll('#list li').forEach(li => li.hidden=!li.textContent.toLowerCase().includes(query));
});
```

Hint: Use includes on lowercased strings.

## 14. Respond to a checkbox

Checkbox state is in checked, not value. Use the disabled property to communicate whether a button is available. The initial UI must match the initial state before a change event occurs.

**Mission:** Enable #continue only while #agree is checked.

```html
<label><input id="agree" type="checkbox"> I agree</label><button id="continue" disabled>Continue</button>
```

```javascript
document.querySelector('#agree').addEventListener('change', e => {
  document.querySelector('#continue').disabled=!e.target.checked;
});
```

Hint: Assign the inverse of checked to disabled.

## 15. Read data attributes

data-* attributes hold string metadata. dataset provides camel-cased property names, such as data-product-id becoming productId. Do not assume a numeric-looking attribute is already a number.

**Mission:** Click #product to show "Product: 42" in #result.

```html
<button id="product" data-product-id="42">View product</button><p id="result" role="status"></p>
```

```javascript
document.querySelector('#product').addEventListener('click', e => {
  document.querySelector('#result').textContent='Product: '+e.currentTarget.dataset.productId;
});
```

Hint: Use currentTarget.dataset.productId.

## 16. Use a one-time listener

The once option removes a listener after its first invocation. It is useful for a one-time introduction or initialization action. It does not disable the element; other listeners can still respond.

**Mission:** Append "Started" to #log only on the first #start click.

```html
<button id="start">Start</button><p id="log" role="status"></p>
```

```javascript
document.querySelector('#start').addEventListener('click', () => {
  document.querySelector('#log').textContent += 'Started';
}, {once:true});
```

Hint: Pass {once:true} as the third argument.

## 17. Remove a listener

removeEventListener requires the same function reference and capture setting used at registration. Two functions with identical text are still different objects. Store a named handler so it can be removed later.

**Mission:** Count #add clicks in #count until #stop is clicked.

```html
<button id="add">Add</button><button id="stop">Stop listening</button><output id="count">0</output>
```

```javascript
let count=0;
const add=document.querySelector('#add');
function increment(){document.querySelector('#count').textContent=++count;}
add.addEventListener('click',increment);
document.querySelector('#stop').addEventListener('click',()=>add.removeEventListener('click',increment));
```

Hint: Keep the increment function reference.

## 18. Respond to Escape

Keyboard events expose key names such as Escape. Scope behavior to the relevant widget instead of taking over every key on the page. This simple panel is not a complete modal dialog; modals also require appropriate semantics and focus management.

**Mission:** Press Escape in #search to clear its value; other keys must leave it alone.

```html
<label>Search <input id="search" value="JavaScript"></label>
```

```javascript
document.querySelector('#search').addEventListener('keydown', e => {
  if(e.key==='Escape') e.currentTarget.value='';
});
```

Hint: Check event.key rather than legacy numeric key codes.

## 19. Build a character counter

String.length counts UTF-16 code units. Array.from splits by code points, which still may not equal user-perceived characters for combined emoji or accents. State your counting rule clearly instead of promising universal character counting.

**Mission:** Show #text.value.length followed by " / 100" in #count after input.

```html
<label>Message <textarea id="text" maxlength="100" aria-describedby="count"></textarea></label><p id="count" role="status">0 / 100</p>
```

```javascript
document.querySelector('#text').addEventListener('input', e => document.querySelector('#count').textContent=e.target.value.length+' / 100');
```

Hint: Use length for this explicitly code-unit-based counter.

## 20. Capstone: a task composer

Combine form handling, safe text insertion and reset behavior. Validate before changing the page, create elements deliberately, and keep feedback visible. A production task app also needs persistence, deletion and an explicit accessibility review.

**Mission:** Submit trimmed, nonempty #task values as li items. Clear the input after success; ignore whitespace-only submissions.

```html
<form id="form"><label>Task <input id="task"></label><button>Add task</button></form><ul id="list"></ul>
```

```javascript
document.querySelector('#form').addEventListener('submit', e => {
  e.preventDefault();
const input=document.querySelector('#task');
const text=input.value.trim();
if(!text)return;
const li=document.createElement('li');
li.textContent=text;
document.querySelector('#list').append(li);
input.value='';
});
```

Hint: Return early for blank input; use textContent for the new item.