Mod 2 - DOM Manipulation

Table of Contents:

Forms

Forms are used for a variety of things. For example:

  • getting user login information
  • a search bar
  • etc...

Forms have a very specific structure:

<form id="pokemon-search-form" aria-labelledby="pokemon-search-header">
  <h2 id="pokemon-search-header">Find a Pokemon</h2>
  <label for="pokemon">Pokemon Name:</label>
  <input type="text" name="pokemon" id="pokemon-input" />
  <button type="submit">Submit</button>
</form>

Every form should have:

  • An id for easy DOM targeting
  • An aria-labelled-by for screen readers referencing the id of the h2 (if there isn't a visible h2, use aria-label)
  • An h2, also for screen readers
  • label and input elements.
    • For every input, you must have an label
    • The id of the input should match the for of the label
  • A button for submitting the form

Q: What is the difference between the id and the name of an input element?

The id is used to connect an input to its label. The name of the input is used when we collect data from the form. If we have an input with the name="email", we can access the value like so:

const form = document.querySelector('form');
const emailValue = form.email.value;
// or
const formObj = Object.fromEntries(new FormData(form));
const emailValue = formObj.email;

Event Listeners / Event Handlers

Adding an event listener to an element allows us to define a callback to execute when an event occurs. This example increments a count each time a button is clicked.

let count = 0;
const button = document.querySelector('#counter-button');
button.textContent = 0;

button.addEventListener('click', (e) => {
  // invoked when the button is clicked
  count++;
  button.textContent = count;
});

Forms need to respond to 'submit' events. Form event handlers typically look like this:

const form = document.querySelector('#pokemon-form');
form.addEventListener('submit', (e) => {
  // stop the reload/redirect
  e.preventDefault();

  // the FormData API makes it SUPER easy to get an object with all form data with 2 steps:
  const formData = new FormData(e.target);
  const formObj = Object.fromEntries(formData);

  // Checkbox data in the formObj will either be "on" or undefined
  // We can convert those values to true/false
  formObj.checkedProperty = !!formObj.checkedProperty

  console.log('here is your data:', formObj);

  // do something with the form

  e.target.reset(); // reset the form
});

The callback provided to addEventListener is invoked with an Event object, typically referenced by a parameter called e or event. That object has information about the event such as the .target of the event which is used here to get the form data.

Q: Why do we use e.preventDefault() when handling a form submission?

The default behavior of a form submission is to refresh the page. The form inputs are also added to the URL as query parameters. Instead, we want to keep the user on the page without reloading so we can handle the form submission with JavaScript.


Event Delegation

Event delegation is the idea that you can have a single event listener on a parent element that can handle events for all of its children.

This is useful for child elements that are added/removed dynamically. Like when a user adds a new item to a list.

<ul id="counting-list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

This example has the ul listening for clicks on the child li Elements. If a click is detected, the number of the li that was clicked is saved and a new li is appended to the list with that number plus 1 (if the li with the number 2 is clicked, we'll add a new li to the list with the number 3).

const ul = document.querySelector('#counting-list');

ul.addEventListener('click', (e) => {
  // check if the target was an li in the counting list
  if (e.target.matches('#counting-list>li')) { 
    const clickedNum = Number(e.target.innerText);
    const li = document.createElement('li');
    li.innerText = clickedNum + 1
    e.currentTarget.append(li);
  }
});

Because the new li Elements are also children of the ul, the new li Elements are also clickable! We don't need to add additional event listeners for each new li because the ul will handle it. It is important to use e.target.matches() to ensure that the target is an element we want to handle clicks on.

Here's the pattern:

  1. Grab a parent element
  2. Have it listen for events, it will detect events triggered by its children because of propagation
  3. Identify the target to decide what you want to do using event.target.matches()

event.target.classList.contains() is also useful for identifying the target.

DOM CRUD Basics

Using the DOM API, we can perform basic CRUD operations:

  • Create new elements and insert them into the DOM
  • Get (Read) an existing element in the DOM
  • Update the properties of existing elements in the DOM
  • Delete existing elements from the DOM

Below are examples of these essential DOM functions.

// Get elements from the DOM using document.querySelector
const someElement = document.querySelector("#some-element-id");
const someList = document.querySelector("ul");
const bodyElement = document.body;

// Create new elements and insert them into the DOM
const someDiv = document.createElement('div');
const img = document.createElement('img')
body.append(someDiv);
someDiv.append(img);

const listItem = document.createElement('li');
someList.append(listItem);

// Update the properties of elements
someList.innerHTML = '';                // clear out the children of the list
listItem.textContent = 'hello world';   // change the inner text content
listItem.classList.add('bolded');       // add a clas
listItem.classList.remove('italic');    // remove a class
listItem.classList.toggle('visible');   // toggle on/off a class
listItem.id = 'first element';          // set an id
img.src = 'somepicture.png';            // set a picture src attribute

// Remove elements from the DOM
someDiv.remove();

Q: When is it appropriate to use .innerHTML to change the contents of an element?

We can use .innerHTML if we are in full control of how we are changing the .innerHTML. For example, we can use it to clear out the contents of an element, or to insert child elements with a known structure. We should NEVER use .innerHTML to insert HTML elements that are in any way generated dynamically by the user unless we first "sanitize" the user data of malicious code. This is called "escaping".


Data Attributes

The syntax is simple. Any attribute on any element whose attribute name starts with data- is a data attribute.

Say you have an article and you want to store some extra information that doesn't have any visual representation. Just use data attributes for that:

<section id="cars">
  <article
    id="electric-cars"
    data-columns="3"
    data-index-number="12314"
    data-parent="cars">
  </article>
  ...
</section>

Reading the values of these attributes out in JavaScript is also very simple. You could use getAttribute() with their full HTML name to read them (.getAttribute('data-columns')), but the standard defines a simpler way: a DOMStringMap you can read out via a dataset property.

To get a data attribute through the dataset object, get the property by the part of the attribute name after data- (note that dashes are converted to camel case).

const article = document.querySelector("#electric-cars");

article.dataset.columns; // "3"
article.dataset.indexNumber; // "12314"
article.dataset.parent; // "cars"

Q: Suppose you had an HTML element <p id='name' data-myName='ben' />My name is ben</p> How would you access the data-my-name attribute value in JavaScript?

const p = document.querySelector("#name");
p.dataset.myName; // "3"

Todo App Challenge

As a challenge, build a todo app as shown below. The application should:

  • have a form with at least two inputs: a text input and a checkbox input.
  • Upon submission, it should
    • Add a new element to a list with the text of the todo and whether or not it was complete.
    • Store the isComplete value as a data- attribute and then style elements with a text-decoration: line-through style based on the data- attribute value
    • In this example, I also print out the data from the form.

A todo app with a text input and a checkbox. New todos are presented in a list with incomplete or complete next to their text. Complete items are striked through.