Table of Contents:
- Forms
- Event Listeners / Event Handlers
- Event Delegation
- DOM CRUD Basics
- Data Attributes
- Todo App Challenge
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 theid
of theh2
(if there isn't a visibleh2
, usearia-label
) - An
h2
, also for screen readers label
andinput
elements.- For every
input
, you must have anlabel
- The
id
of the input should match thefor
of thelabel
- For every
- 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 aninput
to itslabel
. Thename
of the input is used when we collect data from the form. If we have aninput
with thename="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;
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 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:
- Grab a parent element
- Have it listen for events, it will detect events triggered by its children because of propagation
- 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.
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".
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"
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 adata-
attribute and then style elements with atext-decoration: line-through
style based on thedata-
attribute value - In this example, I also print out the data from the form.