JavaScript Core for the Frontend
Build an interactive web page using core JavaScript concepts.
Introduction
30 Second Summary
Every interactive website you use (filtering products, checking off tasks, searching contacts) runs on the same handful of JavaScript patterns. Understanding these patterns is the difference between copying code you don't understand and writing your own confidently.
In this project, you will learn the core JavaScript concepts that power React applications and frontend interviews, then prove your understanding by building an interactive web page entirely from scratch.
What You'll Build
A concept-first project where you learn JavaScript fundamentals and apply them by building your own interactive web page with DOM manipulation, events, and array methods.
By the end of this project, you'll have:
- A working mental model of variables, functions, objects, and arrays and how they connect to React patterns.
- An interactive web page built entirely by you using DOM manipulation, event handling, and dynamic rendering.
- A clear concept map connecting each JavaScript fundamental to its React equivalent and interview relevance.
- Secret Mission: Add dynamic filtering and sorting to your project using array methods and conditional rendering.
Are there any prerequisites?
You should have basic HTML and CSS knowledge. If you can create a simple web page with headings, paragraphs, and styled elements, you have everything you need to start.
Before We Start
Before We Start...
Set Up Your Project Files
Every JavaScript project starts with two things: an HTML file that the browser loads, and a JavaScript file that the HTML file pulls in.
In this step, you will create a project folder, write a minimal HTML page, and link a JavaScript file to it. Then you will confirm the connection is working using Chrome's DevTools Console.
In this step, get ready to:
- Create a project folder on your Desktop and open it in Cursor.
- Create an HTML file linked to a JavaScript file.
- Verify the connection in Chrome DevTools Console.
Create your project folder
You need a dedicated folder to hold your project files. Creating it on the Desktop makes it easy to find later.
- In Cursor, open the integrated terminal by clicking Terminal in the top menu bar, then selecting New Terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir interactive-page
cd interactive-page
- Confirm the folder exists by running:
pwd
You should see a path ending in interactive-page.
- Now open this folder in Cursor. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the interactive-page folder, and click Open.
Why open the folder this way?
When you open a folder in Cursor (instead of a single file), Cursor treats it as your project root. This means the Explorer sidebar shows your full file tree, and the terminal defaults to that folder.
Don't see the folder on your Desktop?
Make sure you ran cd ~/Desktop before mkdir. If you skipped that step, the folder was created in a different location.
Run cd ~/Desktop and try mkdir interactive-page again. Help me find my project folder.
Create the HTML file
The HTML file is the entry point that your browser loads. It provides the page structure and tells the browser where to find your JavaScript.
- In Cursor's Explorer sidebar on the left, hover over the folder name and click the new file icon (a page with a "+" symbol).
- Name the file index.html and press Enter.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Interactive Page</title>
</head>
<body>
<script src="script.js"></script>
</body>
</html>
What does this code do?
This is a minimal HTML5 document. The key parts:
- <!DOCTYPE html> tells the browser to use modern HTML5 standards.
- <meta charset="UTF-8"> ensures text characters display correctly.
- <meta name="viewport" ...> makes the page scale properly on mobile devices.
- <script src="script.js"></script> loads your JavaScript file. It sits at the bottom of the body so the HTML above it loads first.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing a blank file or weird formatting?
Make sure you pasted the code exactly as shown. Check that the <script> tag is inside the <body> (before the closing </body> tag), not inside the <head>.
Help me check my HTML structure.
Connect JavaScript and verify in the browser
Now you will create the JavaScript file that your HTML references. Then you will open the page in Chrome and confirm the two files are talking to each other.
- In Cursor's Explorer sidebar, click the new file icon again.
- Name the file script.js and press Enter.
- Paste the following code into script.js:
console.log("JavaScript is connected!");
What does this line do?
console.log() prints a message to the browser's Developer Tools Console. It is the simplest way to confirm your JavaScript file is loaded and running.
- Save script.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open index.html in Google Chrome. In Cursor's terminal, run:
macOS
open -a "Google Chrome" index.html
Windows
start chrome index.html
- Once Chrome opens, press F12 to open DevTools.
- Click the Console tab at the top of the DevTools panel.
You should see JavaScript is connected! printed in the Console.
Don't see the message in the Console?
Check these common causes:
- Make sure the file name in your script tag (script.js) matches the actual file name exactly. Capitalization matters.
- Confirm both files are saved. Unsaved changes in Cursor show a dot on the file tab.
- Check that both files are in the same folder. If script.js is in a subfolder, the browser cannot find it at the path src="script.js".
Help me debug my JavaScript connection.
✔️ Awesome, I've got everything!
Great. Double check that both files are saved in Cursor before moving on.
ⓧ I'd like to double check the full code
Here is what each file should contain:
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Interactive Page</title>
</head>
<body>
<script src="script.js"></script>
</body>
</html>
script.js:
console.log("JavaScript is connected!");
Your project files are in place and talking to each other. Next up, you will start writing real JavaScript to declare variables, define functions, and build the data layer for your interactive page.
Variables, Data Types, and Functions
Your environment is working. Now it's time to learn the two building blocks that power every JavaScript program: variables (where you store data) and functions (how you do things with that data).
In this step, you'll understand how let, const, and var behave differently, get comfortable with JavaScript's data types, and write functions in three styles. These concepts show up in React code and frontend interviews constantly.
In this step, get ready to:
- Experiment with variables and data types to see how JavaScript stores values.
- Write the same function as a declaration, expression, and arrow function.
- Create the data array for your project using objects and template literals.
Learn and experiment with variables
Every program needs to remember values. JavaScript gives you three keywords for creating variables, and each behaves differently.
What are let, const, and var?
These three keywords declare variables, but they follow different rules:
- let declares a variable you can reassign later. It only exists inside the nearest curly braces (block-scoped).
- const declares a variable that cannot be reassigned. Also block-scoped. Use this by default for everything.
- var is the old way. It ignores block boundaries (function-scoped) and gets hoisted. Avoid it in modern code.
What are primitives and reference types?
Primitive types (string, number, boolean, null, undefined) are simple values. When you copy them, you get an independent copy.
Reference types (objects and arrays) store a pointer to the data. When you copy them, both variables point to the same underlying data. This distinction is critical for React state updates later.
- In script.js, select all existing code and replace it with the following:
// --- Variables and Data Types ---
let score = 10;
score = 20; // reassignment works
const appName = "My App";
// appName = "Other"; // TypeError: Assignment to constant variable
const userName = "Alex"; // string
const userAge = 25; // number
const isLoggedIn = true; // boolean
const nothing = null; // null
let notDefined; // undefined
const userObj = { name: "Alex", age: 25, isActive: true };
const fruits = ["apple", "banana", "cherry"];
console.log(userName, userAge, isLoggedIn, nothing, notDefined);
console.log(userObj);
console.log(fruits);
What does this code do?
- let score shows that let variables can be reassigned. The value changes from 10 to 20 without errors.
- const appName shows that const cannot be reassigned. The commented-out line would throw an error if uncommented.
- The five const/let lines after that demonstrate each primitive type: string, number, boolean, null, and undefined.
- userObj and fruits show reference types (an object and an array).
- The three console.log calls print everything to DevTools so you can verify it works.
Try uncommenting the const reassignment
Remove the // from the line // appName = "Other"; to see the error for yourself. You will see Uncaught TypeError: Assignment to constant variable. in your Console. Then comment it back out so the rest of your code runs.
- Save script.js.
- Switch to Chrome and refresh the page. Open DevTools with F12 and click the Console tab.
- You should see three lines of output: the five primitive values, the userObj object, and the fruits array.
Not seeing any output in the Console?
- Make sure you saved script.js before refreshing the browser.
- If you see a red error about Assignment to constant variable, make sure the appName = "Other" line is commented out with // at the start.
- Check that your script.js file is in the same folder as index.html. The <script src="script.js"> tag looks for the file relative to the HTML file.
Still stuck? Help me debug why nothing appears in my Chrome DevTools Console when I load my HTML file.
Why does this matter for React?
In React, you declare state with const [count, setCount] = useState(0). The const means the binding cannot be reassigned, but React gives you a setter function to update it.
Understanding const with reference types explains why React uses [...array, newItem] to create a new array instead of mutating the old one. React compares references to detect changes.
Learn and experiment with functions
Functions are how you package logic for reuse. JavaScript offers three syntax styles, and each has different behavior around something called hoisting.
What are the three function styles?
- Function declaration: function greet(name) { ... } is hoisted, meaning you can call it before it appears in your code.
- Function expression: const greet = function(name) { ... } is NOT hoisted. You must define it before calling it.
- Arrow function: const greet = (name) => "Hello " + name is concise syntax with no own this binding. When the body is a single expression, you can skip the curly braces and return keyword.
What is a callback function?
A callback is a function passed as an argument to another function. The receiving function decides when to call it. This pattern is behind event handlers, array methods, setTimeout, and React hooks like useEffect.
- Add the following code below your variables section in script.js:
// --- Functions ---
// Declaration (hoisted)
function greetDeclaration(name) {
return "Hello " + name;
}
// Expression (not hoisted)
const greetExpression = function(name) {
return "Hello " + name;
};
// Arrow function
const greetArrow = (name = "World") => `Hello ${name}`;
console.log(greetDeclaration("Alex"));
console.log(greetExpression("Alex"));
console.log(greetArrow());
console.log(greetArrow("Alex"));
What does this code do?
- All three functions do the same thing (greet someone), but use different syntax.
- greetArrow uses a default parameter (name = "World") so it works even without an argument.
- The backtick syntax in the arrow function is a template literal. It lets you embed variables with ${} instead of string concatenation.
- The four console.log calls prove all three styles produce the same result.
- Now add the callback and hoisting demonstrations below your function logs:
// Callback example
function runCallback(callback) {
callback("from callback");
}
runCallback((msg) => console.log("Called " + msg));
// Hoisting demo
console.log(hoistedGreet("World"));
function hoistedGreet(name) {
return "Hoisted hello, " + name;
}
What does this code do?
- runCallback takes a function as its argument and calls it. The arrow function (msg) => console.log("Called " + msg) is the callback that gets executed.
- The hoisting demo calls hoistedGreet BEFORE its definition appears in the file. This works because function declarations are hoisted to the top of their scope by JavaScript's engine.
- Save script.js and refresh Chrome.
- You should see new lines in the Console: Hello Alex (three times), Hello World, Called from callback, and Hoisted hello, World.
Seeing a ReferenceError about a function?
- If you see an error about greetExpression is not defined, check that you did not try to call it before its const line. Only function declarations are hoisted.
- Make sure each function's closing brace } and semicolons are in the right place. A missing semicolon after the function expression (};) can cause unexpected errors.
Help me fix a ReferenceError I'm getting with my function declarations in JavaScript.
Why do interviews ask about hoisting?
Interviewers ask "What is hoisting?" and "Explain the difference between arrow functions and regular functions" in 40-50% of JavaScript interviews. Understanding that declarations are hoisted but expressions are not helps you predict how code executes.
Create the data for your project
Now you'll combine everything into real project data. Your interactive page needs a collection of items to display and manipulate.
You'll create a const array of objects. Each object represents one item in your project (a task, a product, a contact, a quiz question, whatever you chose). Template literals let you log formatted descriptions of each item.
- Add the following code at the bottom of script.js. Customize the name and description values to match whatever project you chose:
// --- Project Data ---
const items = [
{ id: 1, name: "Item One", description: "First item", completed: false },
{ id: 2, name: "Item Two", description: "Second item", completed: true },
{ id: 3, name: "Item Three", description: "Third item", completed: false }
];
items.forEach(item => {
console.log(`Item: ${item.name} \u2014 ${item.description} \u2014 Completed: ${item.completed}`);
});
What does this code do?
- items is a const array of objects. Each object has at least 3 properties (id, name, description, completed). The array is const so it cannot be reassigned, but its contents can still be changed.
- forEach loops over every item and runs the callback once per element. The template literal (backtick string with ${}) builds a formatted sentence for each object.
- This pattern of storing data as an array of objects and iterating over it is exactly how you'll render lists in React.
- Save script.js and refresh Chrome.
- You should see one formatted line per item logged to the Console, showing the name, description, and completed status of each object.
Console shows undefined for your object properties?
- Check that your property names inside the template literal (item.name, item.description) exactly match the keys you defined in each object.
- Make sure you're using backticks (`) for the template literal, not regular quotes. The ${} syntax only works inside backticks.
Help me fix undefined values showing up when I use template literals with my objects.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and that your Console shows all the output from variables, functions, and your project data.
ⓧ I'd like to double check the full code
Here is the complete script.js file at the end of this step:
// --- Variables and Data Types ---
let score = 10;
score = 20; // reassignment works
const appName = "My App";
// appName = "Other"; // TypeError: Assignment to constant variable
const userName = "Alex"; // string
const userAge = 25; // number
const isLoggedIn = true; // boolean
const nothing = null; // null
let notDefined; // undefined
const userObj = { name: "Alex", age: 25, isActive: true };
const fruits = ["apple", "banana", "cherry"];
console.log(userName, userAge, isLoggedIn, nothing, notDefined);
console.log(userObj);
console.log(fruits);
// --- Functions ---
// Declaration (hoisted)
function greetDeclaration(name) {
return "Hello " + name;
}
// Expression (not hoisted)
const greetExpression = function(name) {
return "Hello " + name;
};
// Arrow function
const greetArrow = (name = "World") => `Hello ${name}`;
console.log(greetDeclaration("Alex"));
console.log(greetExpression("Alex"));
console.log(greetArrow());
console.log(greetArrow("Alex"));
// Callback example
function runCallback(callback) {
callback("from callback");
}
runCallback((msg) => console.log("Called " + msg));
// Hoisting demo
console.log(hoistedGreet("World"));
function hoistedGreet(name) {
return "Hoisted hello, " + name;
}
// --- Project Data ---
const items = [
{ id: 1, name: "Item One", description: "First item", completed: false },
{ id: 2, name: "Item Two", description: "Second item", completed: true },
{ id: 3, name: "Item Three", description: "Third item", completed: false }
];
items.forEach(item => {
console.log(`Item: ${item.name} \u2014 ${item.description} \u2014 Completed: ${item.completed}`);
});
Your index.html file is unchanged from Step 1.
You now have a solid grasp of how JavaScript stores and processes data. Next up, you'll learn the array methods that React developers use every single day to transform and filter data.
Objects, Arrays, and Array Methods
In the previous step, you created an array of objects representing your project data. Now you need tools to actually work with that data. Transforming it, filtering it, searching it, and summarizing it.
JavaScript gives you four array methods that handle 90% of data operations in frontend code. These same four methods are the backbone of every React component that displays data. Master them here, and React's data patterns will feel instantly familiar.
In this step, get ready to:
- Use map, filter, find, and reduce to transform and query your data array.
- Practice immutable operations that create new arrays instead of mutating the original.
- Chain array methods together to build complex data pipelines.
Practice the Big 4 array methods
Your items array from Step 2 is a collection of objects. The Big 4 array methods let you do everything you need with collections without writing manual loops.
What are the Big 4 array methods?
Each method takes a callback function and returns something new. None of them change the original array.
- map(callback) transforms every item. Returns a new array of the same length. Use when you want to convert data from one shape to another.
- filter(callback) keeps items that pass a test. Returns a new array (possibly shorter). The callback must return true or false.
- find(callback) returns the first item that passes the test, or undefined if no match. It stops searching after finding one.
- reduce(callback, initialValue) accumulates all items into a single value. Use when you need a total, a grouped object, or any single-value result from an array.
Why do these matter for React?
map() renders lists in React. filter() implements search and filtering. You will use these in literally every React component that handles data.
In interviews, "Implement Array.prototype.map from scratch" and "Explain reduce with an example" are top-tier questions.
Here is your challenge. In script.js, add a new section below your existing code where you:
- Use map() to extract just the names from your items array into a new array.
- Use filter() to get only the completed items.
- Use find() to locate the item with id === 2.
- Use reduce() to calculate a summary value (like counting the total number of items).
- Log each result to the console.
Give it a shot yourself first, then check your work below.
✔️ I'll try it first
Write your own implementations. Save script.js, refresh Chrome, and check the Console for your logged results.
ⓧ Let me check my work
Here is one way to implement the Big 4. Add this below your existing code in script.js:
// --- Array Methods ---
const itemNames = items.map(item => item.name);
console.log("Names:", itemNames);
const completedItems = items.filter(item => item.completed);
console.log("Completed:", completedItems);
const foundItem = items.find(item => item.id === 2);
console.log("Found:", foundItem);
const totalItems = items.reduce((acc, item) => acc + 1, 0);
console.log("Total items:", totalItems);
What does this code do?
- map(item => item.name) pulls just the name property from each object, returning a flat array of strings.
- filter(item => item.completed) keeps only items where completed is true.
- find(item => item.id === 2) stops as soon as it finds the first match and returns that single object.
- reduce((acc, item) => acc + 1, 0) starts with 0 and adds 1 for each item, producing a count.
- Save script.js and refresh Chrome.
- In the Console, you should see four new log lines: an array of names, an array of completed objects, a single found object, and the total count.
Seeing undefined or an empty array?
- Make sure your new code is below the items array declaration. JavaScript runs top to bottom, so the array must exist before you call methods on it.
- For find(), double-check you used === (strict equality), not = (assignment).
Why aren't my array methods returning results?
Practice immutable operations
When you work with arrays in React, you never mutate the original array directly. You always create a new one. This is because React compares references to detect changes. If you mutate the same array, React does not know anything changed and does not re-render.
What does immutability mean in practice?
Immutability means you never use push(), splice(), or direct assignment on your data array. Instead, you create a new array with the change applied.
- To add: use spread [...oldArray, newItem].
- To remove: use filter() to exclude the unwanted item.
- To update: use map() to return a modified copy of one item while keeping the rest unchanged.
Your challenge is to perform all three immutable operations on your items array:
- Add a new item using spread syntax. Create a new object with a unique id and add it to a new array.
- Remove an item using filter(). Filter out the item with id === 2.
- Update an item using map(). Change completed to true for the item with id === 1, leaving others unchanged.
- Log each new array to the console to confirm your original items array is untouched.
Try implementing these yourself, then compare with the reference below.
✔️ I'll try it first
Write your immutable add, remove, and update operations. Save and refresh to confirm you see three new arrays logged, each different from the original.
ⓧ Let me check my work
Add this below your array methods code in script.js:
// --- Immutability ---
const newItem = { id: 4, name: "Item Four", description: "Fourth item", completed: false };
const itemsWithNew = [...items, newItem];
console.log("After add:", itemsWithNew);
const itemsWithoutTwo = items.filter(item => item.id !== 2);
console.log("After remove:", itemsWithoutTwo);
const itemsUpdated = items.map(item =>
item.id === 1 ? { ...item, completed: true } : item
);
console.log("After update:", itemsUpdated);
What does this code do?
- [...items, newItem] spreads all existing items into a new array, then adds newItem at the end. The original items array stays the same.
- filter(item => item.id !== 2) keeps every item except the one with id 2. This is the immutable way to "delete" from an array.
- The map() update uses a ternary: if the id matches, return a new object with completed: true spread over the original. Otherwise, return the item unchanged.
- Save script.js and refresh Chrome.
- In the Console, you should see three new logged arrays: one with 4 items (after add), one with 2 items (after remove), and one where the first item shows completed: true (after update).
Original array getting changed?
- If you used items.push() instead of spread, you mutated the original. Replace it with [...items, newItem].
- Add console.log("Original:", items) at the very end of your file to verify the original array still has exactly 3 items.
Help me understand why my array is being mutated
Chain methods together
Because each array method returns a new array, you can chain them together. Filter first to get a subset, then map that subset into a different shape. This chaining pattern is how you process data in React components.
- Write a chained operation that filters your items array to only incomplete items, then maps those into formatted strings.
- Use template literals to format each item name with a label like "(pending)".
- Log the result to the console.
See if you can write this in a single chained expression, then check below.
✔️ I'll try it first
Chain .filter() and .map() on one statement. Save and refresh to confirm you see an array of formatted strings in the Console.
ⓧ Let me check my work
Add this at the bottom of script.js:
// --- Chaining ---
const incompleteNames = items
.filter(item => !item.completed)
.map(item => `${item.name} (pending)`);
console.log("Incomplete names:", incompleteNames);
What does this code do?
The chain reads left to right, top to bottom. First, filter() narrows the array to items where completed is false. Then map() transforms each surviving item into a template literal string.
The result is ["Item One (pending)", "Item Three (pending)"]. Item Two was filtered out because its completed property is true.
- Save script.js and refresh Chrome.
- In the Console, you should see an array with two formatted strings: "Item One (pending)" and "Item Three (pending)".
Getting an empty array from chaining?
- Check that your filter callback uses !item.completed (with the ! negation) to keep incomplete items.
- Make sure there is no semicolon between .filter() and .map(). The chain must be one continuous expression.
Help me debug my chained array methods
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js and all your console output looks correct.
ⓧ I'd like to double check the full code
Here is the full script.js file at this point. Your variable names and data may differ since you chose your own project, but the structure and methods should match.
// --- Variables and Data Types ---
let score = 10;
score = 20;
const appName = "My App";
const userName = "Alex";
const userAge = 25;
const isLoggedIn = true;
const nothing = null;
let notDefined;
const userObj = { name: "Alex", age: 25, isActive: true };
const fruits = ["apple", "banana", "cherry"];
console.log(userName, userAge, isLoggedIn, nothing, notDefined);
console.log(userObj);
console.log(fruits);
// --- Functions ---
function greetDeclaration(name) {
return "Hello " + name;
}
const greetExpression = function(name) {
return "Hello " + name;
};
const greetArrow = (name = "World") => `Hello ${name}`;
console.log(greetDeclaration("Alex"));
console.log(greetExpression("Alex"));
console.log(greetArrow());
console.log(greetArrow("Alex"));
function runCallback(callback) {
callback("from callback");
}
runCallback((msg) => console.log("Called " + msg));
console.log(hoistedGreet("World"));
function hoistedGreet(name) {
return "Hoisted hello, " + name;
}
// --- Project Data ---
const items = [
{ id: 1, name: "Item One", description: "First item", completed: false },
{ id: 2, name: "Item Two", description: "Second item", completed: true },
{ id: 3, name: "Item Three", description: "Third item", completed: false }
];
items.forEach(item => {
console.log(`Item: ${item.name} \u2014 ${item.description} \u2014 Completed: ${item.completed}`);
});
// --- Array Methods ---
const itemNames = items.map(item => item.name);
console.log("Names:", itemNames);
const completedItems = items.filter(item => item.completed);
console.log("Completed:", completedItems);
const foundItem = items.find(item => item.id === 2);
console.log("Found:", foundItem);
const totalItems = items.reduce((acc, item) => acc + 1, 0);
console.log("Total items:", totalItems);
// --- Immutability ---
const newItem = { id: 4, name: "Item Four", description: "Fourth item", completed: false };
const itemsWithNew = [...items, newItem];
console.log("After add:", itemsWithNew);
const itemsWithoutTwo = items.filter(item => item.id !== 2);
console.log("After remove:", itemsWithoutTwo);
const itemsUpdated = items.map(item =>
item.id === 1 ? { ...item, completed: true } : item
);
console.log("After update:", itemsUpdated);
// --- Chaining ---
const incompleteNames = items
.filter(item => !item.completed)
.map(item => `${item.name} (pending)`);
console.log("Incomplete names:", incompleteNames);
You now have the four most important array methods in your toolkit, plus the immutable patterns that React depends on. Next up, you will put these to work by rendering your data into the actual browser page and making it interactive with events.
DOM Manipulation and Events
You've built a solid data layer. You can create arrays of objects, transform them with map, filter, find, and reduce, and you know how to update them immutably. But none of this shows up on the actual page yet.
In this step, you'll connect your JavaScript to the HTML page using the DOM (Document Object Model). You'll select elements, render your data as visible content, and make the page respond to user interaction through events.
In this step, get ready to:
- Build an HTML structure with a container, input field, and button.
- Render your data array to the page using a reusable function.
- Add event listeners and event delegation for user interactivity.
Build your HTML structure
Before you can display anything with JavaScript, you need HTML elements to target. Your page needs a container to hold the list, an input for adding items, and a button to trigger the addition.
What is DOM selection?
The DOM is the browser's representation of your HTML as a tree of objects. JavaScript can reach into that tree and grab any element using selection methods.
document.querySelector(selector) returns the first element matching a CSS selector, or null if nothing matches. document.querySelectorAll(selector) returns all matching elements as a NodeList.
- In Cursor, open index.html and find the empty <body> section above the <script> tag.
- Add the following HTML elements above the <script src="script.js"></script> line:
<h1>My Interactive List</h1>
<input type="text" id="item-input" placeholder="Enter item name" />
<button id="add-btn">Add Item</button>
<ul id="item-list"></ul>
What do these elements do?
- The <input> with id="item-input" gives you a text field the user types into.
- The <button> with id="add-btn" triggers adding a new item when clicked.
- The <ul> with id="item-list" is your empty container. JavaScript will fill it with list items dynamically.
- Save index.html.
- Open the file in Chrome. You should see a heading, a text input, and an "Add Item" button on the page.
Don't see the input and button?
Make sure the new HTML is placed above the <script> tag, not below it. The script tag must remain the last element in the body.
help me figure out why my HTML elements are not showing up
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Interactive Page</title>
</head>
<body>
<h1>My Interactive List</h1>
<input type="text" id="item-input" placeholder="Enter item name" />
<button id="add-btn">Add Item</button>
<ul id="item-list"></ul>
<script src="script.js"></script>
</body>
</html>
Now you need to connect JavaScript to these elements. Your previous script.js was a learning playground. Those experiments served their purpose. Now you'll replace the file contents with your actual project code.
- Open script.js in Cursor.
- Select all the existing code (press Cmd+A on macOS or Ctrl+A on Windows) and delete it.
- Replace it with your project data and DOM references by pasting this code:
// --- Project Data ---
let items = [
{ id: 1, name: "Item One", description: "First item", completed: false },
{ id: 2, name: "Item Two", description: "Second item", completed: true },
{ id: 3, name: "Item Three", description: "Third item", completed: false }
];
// --- DOM References ---
const itemList = document.querySelector("#item-list");
const itemInput = document.querySelector("#item-input");
const addBtn = document.querySelector("#add-btn");
What does this code do?
- items is now declared with let instead of const. You need to reassign it later when adding or removing items immutably.
- Each document.querySelector() call grabs a reference to one of your HTML elements using its CSS selector (the # targets an element by its id attribute).
- Storing these in const variables means you only look them up once, then reuse the reference throughout your code.
- Save script.js and refresh your page in Chrome.
- Open the Console (press F12, then click the Console tab). You should see no errors. The page shows the heading, input, and button, but the list is still empty.
Seeing an error about null?
If you see something like "Cannot read properties of null," it means querySelector returned null. Double-check that the id values in your HTML match the selectors in your JavaScript exactly.
Also confirm that your <script> tag is at the bottom of the body, after all the HTML elements. If the script runs before the elements exist, the selectors will find nothing.
help me debug my querySelector null error
Render your data to the DOM
You have data in JavaScript and an empty container in HTML. A render function bridges the gap. It takes your data, converts it to HTML strings, and injects them into the page.
What is innerHTML?
element.innerHTML lets you set the HTML content inside an element. When you assign a string of HTML to it, the browser parses that string and renders it as real DOM elements.
This is the same pattern React uses under the hood. React's map() inside JSX is doing exactly this: transforming data into renderable output.
- Add the following render function below your DOM references in script.js:
// --- Render Function ---
function renderItems(data) {
itemList.innerHTML = data
.map(
item => `<li data-id="${item.id}">
<span>${item.name} \u2014 ${item.description} \u2014 ${item.completed ? "Done" : "Pending"}</span>
<button class="delete-btn" data-id="${item.id}">Delete</button>
<button class="toggle-btn" data-id="${item.id}">Toggle</button>
</li>`
)
.join("");
}
renderItems(items);
What does this code do?
- renderItems(data) takes an array of items and converts each one into an HTML string using map() and template literals.
- data-id attributes store each item's ID on the HTML element itself. This lets you identify which item was clicked later.
- The ternary operator (condition ? "Done" : "Pending") displays different text based on the completed property.
- .join("") combines the array of HTML strings into one big string. Without it, you'd see commas between each list item.
- renderItems(items) at the bottom calls the function immediately so your data appears when the page loads.
- Save script.js and refresh your browser.
- You should see three list items on the page, each showing the item name, description, and status (Done or Pending). Each item also has a Delete and Toggle button.
List not appearing?
Check the Console for errors. If you see a syntax error, make sure your template literal uses backticks (the key to the left of 1), not regular quotes.
If you see "itemList is not defined," your render function is above the DOM references section. Make sure it comes after the const itemList = document.querySelector("#item-list") line.
help me debug why my list isn't rendering
Why a reusable render function?
Every time your data changes (adding, deleting, or toggling an item), you call renderItems(items) again to refresh the page. This "data changes, then UI re-renders" pattern is exactly how React works. React just automates the re-render call for you.
Add interactivity with events
Your page displays data, but it does not respond to the user yet. Event listeners are how JavaScript reacts to clicks, keypresses, and other user actions.
What is addEventListener?
element.addEventListener(type, handler) attaches a function (the handler) that runs whenever the specified event fires on that element. The type is a string like "click", "input", or "submit".
The handler receives an event object with useful properties. event.target is the element the user actually interacted with. event.preventDefault() stops default browser behavior (like a form submission refreshing the page).
- Add the following event listener below your renderItems(items) call in script.js:
// --- Add Item ---
addBtn.addEventListener("click", () => {
const name = itemInput.value.trim();
if (!name) return;
const newItem = {
id: Date.now(),
name,
description: "New item",
completed: false
};
items = [...items, newItem];
itemInput.value = "";
renderItems(items);
});
What does this code do?
- itemInput.value.trim() reads what the user typed and removes leading/trailing spaces.
- if (!name) return exits early if the input is empty. This prevents adding blank items.
- Date.now() generates a unique numeric ID based on the current timestamp.
- items = [...items, newItem] creates a new array with all existing items plus the new one. This is immutable. The old array is not mutated.
- After updating the data, renderItems(items) refreshes the page to show the new state.
- Save script.js and refresh your browser.
- Type a name into the input field and click Add Item. You should see a new item appear at the bottom of the list with "Pending" status.
Nothing happens when you click Add Item?
Open the Console and check for errors. If you see "addBtn is null," the button ID in your HTML (add-btn) does not match the selector in your JavaScript. Make sure both are exactly #add-btn.
help me debug my Add Item button
Adding items works. Now you need the Delete and Toggle buttons to respond to clicks. This is where event delegation comes in.
What is event delegation?
When you click a child element, the event "bubbles" up through every ancestor element. Event delegation takes advantage of this. Instead of attaching a listener to every button individually, you attach ONE listener to the parent container.
Inside the handler, event.target tells you which specific child was clicked. This is more efficient and works automatically for dynamically added elements. It is the pattern behind React's synthetic event system.
- Add the event delegation listener below your add-item listener in script.js:
// --- Event Delegation ---
itemList.addEventListener("click", (event) => {
const id = Number(event.target.dataset.id);
if (event.target.classList.contains("delete-btn")) {
items = items.filter(item => item.id !== id);
renderItems(items);
}
if (event.target.classList.contains("toggle-btn")) {
items = items.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
);
renderItems(items);
}
});
What does this code do?
- One listener on itemList handles clicks on all buttons inside it. No matter how many items you add, this single listener covers them all.
- event.target.dataset.id reads the data-id attribute from whichever button was clicked. Number() converts it from a string to a number so it matches your item IDs.
- event.target.classList.contains() checks which type of button was clicked (delete or toggle).
- Delete uses filter() to create a new array without the matching item. Toggle uses map() to flip the completed property. Both are immutable updates.
- Save script.js and refresh your browser.
- Click a Delete button. The item disappears from the list.
- Click a Toggle button. The item's status switches between "Done" and "Pending."
- Add a new item, then delete it. Everything works together.
Delete or Toggle buttons not working?
If clicking does nothing, check that the class names on your buttons (delete-btn and toggle-btn) match exactly what your event delegation checks with classList.contains().
If the wrong item gets deleted, make sure each button's data-id attribute is set to ${item.id} inside the template literal.
help me debug my event delegation
Why is this an interview favorite?
"Explain event bubbling" and "What is event delegation?" appear in nearly every frontend interview. Interviewers want to know you understand that events propagate up through the DOM, and that you can leverage this for cleaner, more efficient code.
You just implemented both concepts. event.target (the element that was clicked) and event.currentTarget (the element the listener is attached to) are the two key properties interviewers expect you to distinguish.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
// --- Project Data ---
let items = [
{ id: 1, name: "Item One", description: "First item", completed: false },
{ id: 2, name: "Item Two", description: "Second item", completed: true },
{ id: 3, name: "Item Three", description: "Third item", completed: false }
];
// --- DOM References ---
const itemList = document.querySelector("#item-list");
const itemInput = document.querySelector("#item-input");
const addBtn = document.querySelector("#add-btn");
// --- Render Function ---
function renderItems(data) {
itemList.innerHTML = data
.map(
item => `<li data-id="${item.id}">
<span>${item.name} \u2014 ${item.description} \u2014 ${item.completed ? "Done" : "Pending"}</span>
<button class="delete-btn" data-id="${item.id}">Delete</button>
<button class="toggle-btn" data-id="${item.id}">Toggle</button>
</li>`
)
.join("");
}
renderItems(items);
// --- Add Item ---
addBtn.addEventListener("click", () => {
const name = itemInput.value.trim();
if (!name) return;
const newItem = {
id: Date.now(),
name,
description: "New item",
completed: false
};
items = [...items, newItem];
itemInput.value = "";
renderItems(items);
});
// --- Event Delegation ---
itemList.addEventListener("click", (event) => {
const id = Number(event.target.dataset.id);
if (event.target.classList.contains("delete-btn")) {
items = items.filter(item => item.id !== id);
renderItems(items);
}
if (event.target.classList.contains("toggle-btn")) {
items = items.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
);
renderItems(items);
}
});
Your page is alive. It renders data, responds to input, and updates immutably. This is the exact data-to-UI loop that React automates. Next up, you'll push this further in the Secret Mission by adding filtering and sorting that derive new views from your data without ever mutating the source.
Secret mission
Add Dynamic Filtering and Sorting
Your list works, but real apps let users find what they need instantly. Can you add live search filtering and sorting that derives new views from your data without ever mutating the original array? This is exactly the pattern React uses internally.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about.
Resources you used:
- Your project folder containing index.html and script.js.
Keep everything running
No action needed. Choose this if you want to keep experimenting with your project or plan to extend it further.
- Your project folder stays on your Desktop, ready to open in Cursor any time.
- There are no background processes or services running from this project.
Pause - I'll come back to this later
Since this is a local project with no running processes, pausing is the same as keeping everything. Your files sit on disk until you return.
- Close the Chrome tab with your project open.
- Close the project folder in Cursor.
- Your files remain on your Desktop for when you come back.
Delete - I don't want to use this again
Remove all project files from your machine.
- Close the Chrome tab and the project in Cursor.
- Delete your project folder by running the command for your operating system below.
macOS
cd ~/Desktop
rm -rf your-project-folder
Windows
cd ~\Desktop
rmdir /s /q your-project-folder
Replace your-project-folder with the actual name of the folder you created in Step 1.
Nice Work!
Nice Work!
You just built an interactive web page from scratch using core JavaScript concepts. Every pattern you practiced here maps directly to how React works under the hood.
You've learned how to:
- Declared variables with let and const, written functions in three styles (declaration, expression, arrow), and used callbacks to pass behavior between functions.
- Created and transformed data using the Big 4 array methods (map, filter, find, reduce) with immutable patterns that prevent state mutation bugs.
- Built a fully interactive page using DOM manipulation, event listeners, and event delegation to render data dynamically and respond to user input.
- Secret Mission: Added dynamic filtering and sorting that derives new views from your data without mutation, exactly the pattern React uses internally.
Ready to quiz yourself?