2. Dynamic Content
Follow along with code examples here!
Table of Contents
Key Concepts
Create, Modify, Append — a pattern for dynamically creating content using the
document.createElement()andelement.appendmethods.Event propagation (bubbling) — when an event occurs on an element, it "bubbles up" through all of its ancestors, allowing them to also detect the event.
Event delegation — a pattern where a parent element handles events for its children, using
event.targetto determine which child triggered the event.
Key Syntax
document.createElement(tagName)— creates a new HTML element of the specified type (e.g.,'div','li','img'). The element is not yet on the page.parentElement.append(childElement)— adds an element as the last child of the parent element. You can append multiple elements at once:parent.append(el1, el2, el3).event.currentTarget— the element that the event listener is attached to (the one handling the event).event.stopPropagation()— prevents an event from bubbling up to parent elements.element.matches(selector)— returnstrueif the element matches the given CSS selector. Useful for filtering events in delegation.element.closest(selector)— returns the nearest ancestor (or itself) that matches the selector. Useful for finding a parent container from a clicked child.document.querySelectorAll(selector)— returns aNodeListof all elements matching the CSS selector.NodeList— an array-like collection of elements. SupportsforEachand bracket notation, but not all array methods likemaporfilter. Use[...nodeList]to convert to a true array.element.innerHTML— a property that gets or sets the HTML content inside an element. Convenient but risky with user-generated content (XSS vulnerability).element.removeEventListener(eventType, handler)— removes an event listener. Requires a reference to the original handler function.
Why Create Elements Dynamically?
In the last lesson, we learned how to select existing elements and make them interactive. But what if the elements don't exist yet?
Consider these real-world scenarios:
A todo list where users can add new items
A shopping cart that shows items the user has added
A search results page that displays matches based on user input
A social media feed that loads new posts as you scroll
In all these cases, we can't hardcode the HTML—the content depends on data that may change. We need to create elements dynamically using JavaScript.
The Create, Modify, Append Pattern
To add new elements to the page, follow this three-step pattern:
Create — use
document.createElement(tagName)to make a new elementModify — set its properties (
textContent,id,classList,src, etc.)Append — use
parentElement.append(newElement)to add it to the page
The element doesn't appear on the page until you append it. Until then, it only exists in memory.
Appending Multiple Elements At Once
You can also append multiple elements at once using append:
Challenge: Add a list item when clicking a button
In the index.html file is a button above the list:
Make the button interactive such that when you click on it, a new list item is generated with the next number in the sequence (if the last number is 3, the next number should be 4, then 5 and so on).
Rendering Data from Arrays
We can see the power of this create-modify-append pattern when we have an array of data that we want to generate our HTML content from.
Consider this array:
Q: How would you use it to create this HTML structure?
We just use a loop to repeat the create → modify → append pattern:
Challenge: Rendering a list of movies
Now consider a more complex array, like an array of movies:
Q: How would you use this data to create the following HTML?
Ask yourself:
What is the parent element?
What elements do I need to create?
How will I modify those elements?
What elements will I need to append?
Note: we aren't using the fun fact yet!
Solution
The pattern is the same:
Grab the parent ul
Create the needed elements (
li,img,h3,p)Modify them
Append to assemble the elements
Making Dynamic Content Interactive
We saw how to make a button interactive such that it can dynamically create an element.
But what about also making the new elements that we create interactive? For example, let's say we want to highlight a movie when clicked.
The Problem: Adding Listeners to Dynamic Elements
One approach is to add an event listener to each element as we create it. When that li is clicked, we'll toggle on/off the "highlight" class:
This works, but it is inefficient. Why? Consider this question:
Q: How many event listeners have we created?
Every element has its own listener:
If we have three list items, we will have three separate event listeners.
If we have a million list items, we will have a million event listeners.
It is more efficient to have a single event listener on the parent element (the ul) and have it handle click events on any of its children.
This is possible through event bubbling (propagation) and event delegation
Event Bubbling (Propagation)
When an event occurs on an element, it doesn't just fire on that element—it "bubbles up" through all of its ancestors:
If you click the button, all three handlers fire (from innermost to outermost):
The event "bubbles up" from the button, through #inner, to #outer.
When handling a bubbled event, you can use two properties to understand what happened:
event.target— the element that originally triggered the event (the button)event.currentTarget— the element handling the event (where the listener is attached)
Event Delegation: The Solution
Event delegation uses bubbling to our advantage. Instead of adding listeners to each child element, we add one listener to the parent and use the event.target to tell us which child was clicked:
event.target will be the element inside of our movies list that was clicked, but that could be any element: the p, the h3, the li, or even the ul itself. Whatever we click on will be highlighted.
element.closest
element.closestInstead, we want to highlight the entire li element that was clicked. This is where the element.closest() method comes in:
The closest() method is key here—it finds the nearest ancestor (or the element itself) that matches the selector. This handles cases where the user clicks on the <h3> or <p> inside the <li>.
Dataset: Storing Data On Elements
Look in the HTML 1-rendering-data/index.html file and you'll see an empty #fun-fact paragraph element.
Each movie object in the movies array has a fun fact. Suppose that when a user clicks on a movie list item, we want to display the corresponding movie's fun fact in that element.
Problem: Using the textContent to find the movie
Our event handler has no easy way to access that data. We'd have to jump through quite a few hoops, using the text content of the elements inside the movie list item to find the right object:
Solution: data- Attributes and dataset
data- Attributes and datasetdata- is an attribute that can be added to any element to store custom data on the element:
When creating elements dynamically, you can set these data- attributes (and any attribute really) using the dataset property:
Important: When using dataset, use camelCase (e.g., funFact), but the actual HTML attribute will be kebab-case (data-fun-fact). JavaScript automatically converts between them.
In an event handler, you can then read the stored data using the dataset property:
Note that we stored the index. This allows us to easily find the entire movie object for more data access.
Challenge: Card Flip Game
Build a card flip game that lets users click cards to reveal hidden emojis. HTML and CSS has been provided for you as well as a set of shuffled emojis:
index.html
index.js
Requirements:
Render cards from an array of emojis (the emojis are duplicated for matching pairs)
Each card should start face-down (emoji hidden) and flip to reveal the emoji when clicked
Use CSS to hide/show the emoji (the class
.cardwhen face-down, and.card.flippedwhen flipped)Use event delegation—one click listener on the parent container handles all card clicks
When you're done rendering and adding your event listener, the card grid should look like this after clicking on two of the cards:
Bonus Challenges:
Only allow 2 cards to be flipped at a time. If they match, keep them flipped. If not, flip them back after 1 second.
Add a move counter that increments each time the user flips a card
Add a win condition that displays a message when all pairs are matched
Add a "Reset" button that shuffles and resets all cards
Additional Reading
Selecting Multiple Elements with querySelectorAll
While querySelector returns the first matching element, querySelectorAll returns all matching elements as a NodeList:
A NodeList is array-like but not a true array. You can access elements by index and use forEach:
But you cannot use map, filter, or other array methods directly:
To use array methods, spread the NodeList into an array first:
innerHTML: A Shortcut with Risks
Instead of creating elements one by one, you can inject HTML directly using innerHTML:
This is more concise, but comes with serious security risks if used with user-generated content:
This is called a Cross-Site Scripting (XSS) attack. The malicious code runs in the user's browser.
Rules for innerHTML:
Safe: hardcoded strings that you control
Dangerous: any data from users, APIs, or databases
When in doubt, use createElement and textContent—they're safer because textContent escapes HTML characters automatically.
element.matches
element.matchesWith event delegation, you can use element.matches(selector) to handle events for specific targets:
Stopping Event Propagation
Sometimes you don't want an event to bubble up to parent elements. You can stop propagation using event.stopPropagation():
When to use it:
When a child element should handle an event exclusively
When you have nested interactive elements and don't want clicks to trigger multiple handlers
When NOT to use it:
Don't use it "just in case"—it can break event delegation and other patterns that rely on bubbling
If you're using event delegation, you typically don't need
stopPropagation
Removing Event Listeners
You can remove event listeners using removeEventListener. This requires a reference to the original handler function:
Important: You cannot remove an anonymous function because you have no reference to it:
When to remove listeners:
When an element is being removed from the page (though modern browsers handle this automatically)
When you want to disable interactivity temporarily
When implementing "one-time" actions (though you can also use
{ once: true }option)
Last updated