Skip to main content

JavaScript Real World Vademecum

Part III: DOM, Events & Network

JavaScript comes alive when it interacts with the Browser. Here we learn how to manipulate the HTML (DOM), listen to user actions (Events), and communicate with external servers (Async/Fetch).


DOM and Interactivity

21. Script Loading

If the DOM is the "house" (the HTML structure) that the browser builds, your JavaScript file is the "electrical system" and the "appliances" (the interactivity). The fundamental question is: when do you let the electrician in?

<script> Tag Placement

Think of the browser as a worker 👷‍♂️ who reads the instructions (your HTML file) from top to bottom, one line at a time, and builds the house.

The Problem: <script> in <head> (The Wrong "Old" Way)

If you put the electrician (<script>) in the <head> (the foundations), disaster happens:

  1. The worker reads <html>, reads <head>.
  2. Finds <script src="app.js">.
  3. The worker stops. It immediately stops building the house. This is called render-blocking.
  4. Goes to look for the electrician’s van (downloads the app.js file).
  5. Waits for the electrician to do all their work (executes the app.js file).
  6. Only then, the worker goes back to reading the instructions and building the <body> (the walls, the rooms, the furniture).

Why is it a disaster? If in your app.js it says document.getElementById("button"), the script is executed at step 5, but the "button" (which is in the <body>) doesn’t exist yet! It will only be built at step 6. Your script will look for a button in a house with no walls. Result: null and an application crash.

The "Classic" Solution: <script> at the End of <body>

For decades, the solution has been to put the electrician as the last thing to do:

    ...
<button id="button">Click me</button>
</body>
<script src="app.js"></script> </html>
  • How it works: The worker builds the entire house (<head>, <body>, button, everything). Only when it’s finished, as the very last instruction, it lets the electrician in.
  • Advantage: The electrician comes in and sees the whole house already built. document.getElementById("button") works 100%.
  • Disadvantage: If the electrical system is huge (a 5MB JS file), the user will see a house built but turned off (not interactive) for seconds, until the electrician’s van finishes downloading.

defer Attribute (Best Practice)

defer is the modern and smart solution. It’s a special instruction you give to the worker.

Think of defer as telling the worker: "See that electrician’s van? Go ahead and have it unload all the cables while you keep building the walls (non-blocking download). They can come in to connect the wires only when you’ve finished the structure, but don’t worry about waiting for them to finish polishing the outlets" (executes after parsing, before DOMContentLoaded).

You use it like this, in the <head>:

<head>
...
<script src="app.js" defer></script>
</head>
  • How it works:

    1. The browser sees <script defer> in the <head>.
    2. It starts downloading app.js in the background.
    3. IT DOES NOT STOP! It keeps building the <body> (it’s not render-blocking).
    4. When it has finished reading all the HTML, it executes the app.js script (which was already ready).
    5. Finally, it declares the house ready (the DOMContentLoaded event).

It’s the best of both worlds: the download starts early, it doesn’t block building the house, and execution is guaranteed after all elements exist.

And async? async is another attribute, but it’s an "undisciplined electrician". It downloads in parallel, but then interrupts the worker and runs the code as soon as it finishes downloading, even if the house is half built. It’s useful for scripts that don’t depend on the DOM (e.g., Google Analytics), but defer is almost always the best and safest choice for your main scripts.


window.onload vs DOMContentLoaded

These are events, they’re the "work permits" that tell your code: "OK, now you can start".

  • DOMContentLoaded (The Architect’s Permit) This event fires as soon as the worker has finished reading the instructions (HTML) and has built the structure (the DOM).

    Analogy: It’s the architect saying: "The load-bearing walls and the roof are up. The house is structurally ready." What it does not wait for: It doesn’t wait for the furniture to be delivered (images), for the curtains to be hung (CSS), or for the tenants to arrive (iframe). Why it’s the best: It’s fast. Your JavaScript can make the page interactive much earlier than when all the heavy images finish loading.

    // Use it like this:
    window.addEventListener('DOMContentLoaded', () => {
    // The DOM is ready!
    const button = document.getElementById("button");
    button.addEventListener("click", () => alert("Hi!"));
    });

    (If you use defer, your script is executed right before this event, so often you don’t even need to wait for it!)

  • window.onload (The Homeowner’s Permit) This event is the "old way". It’s much slower and more patient. It fires only when literally everything has loaded.

    Analogy: It’s the homeowner saying: "Ok, the house is finished, the furniture is inside, the curtains are hung, the lights work, and the garden is planted. Now you can come in." The Problem: If you have a gallery of 50 heavy images, your JavaScript (and your app) will stay "frozen" and non-interactive until the last image has been downloaded. It’s terrible for user experience (UX).

    // Old style (avoid unless for specific reasons)
    window.onload = () => {
    // Everything (including images) is loaded.
    };

Practical Rule: Always use <script defer> in the <head>. If for some reason you can’t, put your code inside a DOMContentLoaded listener. Avoid window.onload unless you really need to wait for images to finish loading.





22. DOM Manipulation - The Bridge to the Browser

If your HTML file is the blueprint of a house, the DOM (Document Object Model) is the real house built by the browser. It’s a living and interactive structure made of objects.

Your JavaScript, therefore, does not read the HTML file. Your JavaScript is the interior designer, the electrician, and the demolition crew that enter the already built house (the DOM) to move furniture, paint walls, and install switches.

Manipulating the DOM is the act of using JavaScript to modify this house.

Element Selection

Before you can paint a wall, you have to find it. Selection is like giving a precise address to your work crew.

  • getElementById(id) (The Fastest) This is like having an element’s tax ID code. It’s the fastest and most direct way to find a unique element.

    // HTML: <div id="main-header">...</div>
    // Note: you don’t need the '#' in the name!
    const header = document.getElementById("main-header");
  • querySelector(selector) (The Modern GPS) This is the most flexible and modern tool. It’s like a GPS: you can give it any kind of address (a CSS selector) and it will find the first element that matches.

    // Find by ID
    const header = document.querySelector("#main-header");
    // Find by class (the first one)
    const firstButton = document.querySelector(".btn-primary");
    // Find by tag and attribute
    const emailInput = document.querySelector('input[type="email"]');
  • querySelectorAll(selector) (The Census) Similar to the GPS, but it does a census: it creates a list (a NodeList) of all elements that match the selector.

    const allButtons = document.querySelectorAll(".btn");
    // Now you can loop over them:
    allButtons.forEach(button => {
    button.style.color = "red";
    });
  • getElementsByClassName(class) (The Old "Live" Method) This method, like getElementsByTagName, is the "old way". It’s still useful, but it has an important quirk: it returns an HTMLCollection.

    • The Quirk (Live HTMLCollection): Think of an HTMLCollection like a security camera feed. It’s live. If a new element with that class is added to the page after you ran the command, it will magically appear in your variable!
    • A NodeList (from querySelectorAll) is instead a photograph. It’s static. It doesn’t update if the page changes.
    • Drawback: HTMLCollection is not a real array. It lacks modern methods like .forEach().
  • Converting NodeList / HTMLCollection into an Array To use the power of array methods (.map, .filter, etc.) on a NodeList or HTMLCollection, you first need to "turn it" into a real array. Analogy: It’s like taking raw data from the camera feed and printing it onto sheets of paper that you can manipulate.

    // Modern method (Spread Operator '...')
    const elementsArray = [...document.getElementsByClassName("my-class")];

    // Formal method
    const elementsArray2 = Array.from(document.querySelectorAll(".btn"));

    // Now you can use .map()!
    elementsArray.map(el => el.textContent);

Properties vs Methods (The Distinction)

This is a fundamental philosophical distinction. How do you tell a noun from a verb?

  • Properties (The Nouns: "What it is") These are an element’s data. They are values you read or assign using the = symbol. They don’t have parentheses (). Analogy: height, color, weight, written text.

    // Assign a value to a property
    element.textContent = "New text";
    input.value = "Default value";
    element.id = "new-id";
  • Methods (The Verbs: "What it does") These are the actions an element can perform. You call them using parentheses (). Analogy: click!, addChild!, remove yourself!.

    // Call a method
    element.addEventListener("click", myFunction);
    element.remove();
    container.appendChild(newElement);

The Recurring Mistake: Confusing the two is the most common error.

// WRONG ❌
element.textContent("Text"); // Error: textContent is not a function!

// CORRECT ✅
element.textContent = "Text"; // It’s a property, you assign it!

// WRONG ❌
element.addEventListener = "click"; // Error: addEventListener is a method, it must be called!

// CORRECT ✅
element.addEventListener("click", () => {}); // You call it with ()!

HTML Attributes vs DOM Properties

This one is subtle. What’s the difference between the blueprint and the real house?

  • HTML Attribute (The Blueprint): It’s what you write literally in your .html file. It’s the initial declaration. <input type="text" value="Initial Value">

  • DOM Property (Physical Reality): It’s the live property on the JavaScript object that the browser creates. const input = document.querySelector("input"); input.value; // "Initial Value"

Most of the time, properties and attributes are "reflected": if you change one, the other changes too. input.id = "test" -> The HTML is now <input id="test">

The Crucial Difference (with value): What happens if the user types in the input?

  1. The user types "Hi".
  2. The property input.value (reality) becomes "Hi".
  3. The HTML attribute (the blueprint) stays "Initial Value"!

The attribute represents the default value (what you load), the property represents the current value. Practical Rule: Always work with properties (e.g. input.value, input.id, input.className). It’s faster and more direct. Use element.setAttribute() only for custom attributes (like data-*) or when you want to modify the blueprint’s "default value".


Creating and Adding Elements

You’re not limited to modifying what exists. You can build new rooms!

  • document.createElement(tag) (The Factory) This command creates a new element in memory (in the "warehouse"), not yet on the page.

    const newParagraph = document.createElement("p");
    newParagraph.textContent = "I’m new!";

    At this moment, newParagraph exists, but the user can’t see it. It’s a wall built in a factory, waiting to be installed.

  • container.appendChild(element) (The Installation) This is the act of taking the created element and "installing" it in the house. It adds it as the last child of the container element.

    // Find the "room" where you want to install it
    const container = document.querySelector("#main-content");

    // Install the wall
    container.appendChild(newParagraph);
    // NOW the user can see it!

Modifying Content

There are different ways to change what’s written inside an element, each with its pros and cons.

  • textContent (The Safe Choice ✅)

    • What it does: Sets or reads only the plain text inside an element. It ignores any HTML.
    • Analogy: It’s like writing on a wall with a marker. If you try to write <strong>Hi</strong>, you will literally see the text "Hi" on the wall.
    • Why use it: It’s fast and 100% safe against XSS (Cross-Site Scripting) attacks. If a user writes malicious code (<script>...) in their name, textContent will treat it as harmless text.
    element.textContent = "Hello world!";
  • innerHTML (The Powerful and Dangerous Choice ⚠️)

    • What it does: Sets or reads the full HTML inside an element.
    • Analogy: It’s like a magical Harry Potter pen. If you write <strong>Hi</strong>, it creates actual bold text.
    • The Danger: If you write <script>... (maybe coming from user input), the magical pen will execute it. This is a huge security hole.
    • Rule: Use innerHTML ONLY if 1) you wrote the HTML yourself, or 2) the source is 100% safe. Never use it to display data entered by a user.
  • innerText (The Confusing Choice ❓)

    • What it does: It’s the "weird cousin". It tries to read the text as the user sees it. It ignores elements hidden by CSS (display: none) and interprets spaces and line breaks.
    • Why avoid it: It’s much slower than textContent (it has to compute CSS layout) and has unpredictable behavior. 99% of the time, you want textContent.
  • insertAdjacentHTML(position, htmlText) (The Surgeon) It’s like innerHTML, but more precise. Instead of replacing everything, it lets you "inject" HTML at 4 precise positions relative to the element:

    1. 'beforebegin': Before the element.
    2. 'afterbegin': Inside the element, before everything else.
    3. 'beforeend': Inside the element, after everything else (like appendChild).
    4. 'afterend': After the element.

Modifying Styles

As with content, you have two ways: the direct (and dirty) one and the clean (and preferred) one.

  • style.property (camelCase) (The Direct/Dirty Way) It lets you set inline styles (the ones in the style="..." attribute). Analogy: Grabbing a bucket of paint and throwing it on the wall with your hands. It works, but it’s a mess to clean up and it overrides everything.

    const element = document.querySelector("#warning");

    // NOTE: 'background-color' (CSS) becomes 'backgroundColor' (JS)
    element.style.backgroundColor = "red";
    element.style.fontSize = "20px";
    element.style.display = "block"; // To show
    element.style.display = "none"; // To hide

    Drawback: It creates inline styles, which have a very high "specificity" and are hard to override with your CSS files. It mixes logic (JS) with presentation (CSS).

  • classList (Best Practice ✅) This is the professional way. Analogy: Instead of painting the wall, you attach a label to the wall that says "important-wall". Then, in a separate stylesheet (CSS), you define .important-wall { background-color: red; }. This separates responsibilities: JavaScript manages state (the label), CSS manages appearance.

    // CSS:
    // .is-hidden { display: none; }
    // .is-active { color: blue; }

    // JS:
    element.classList.add("is-active"); // Adds the class
    element.classList.remove("is-hidden"); // Removes the class
    element.classList.toggle("highlighted"); // Adds if missing, removes if present
    element.classList.contains("is-active"); // true

    It’s cleaner, easier to maintain, and it doesn’t mess up CSS specificity.


Modifying Attributes/Properties

  • disabled Property (true/false) This is a fundamental boolean property for forms. It’s the On/Off switch for an input.

    const myButton = document.querySelector("#submit-btn");

    // To disable (the user can’t click)
    myButton.disabled = true; // Appears gray, not clickable

    // To re-enable
    myButton.disabled = false;

    When an input is disabled, its value is not submitted with the form.

  • disabled vs readonly This is a key distinction for UX:

    • disabled: The element is off. The user can’t click it, can’t focus it (with Tab), and its value is not submitted. It’s a wall.
    • readonly: The element is locked read-only. The user can see it, can focus it (and copy its text), but can’t change it. Its value is submitted with the form.

    Analogy: disabled is a barricaded door. readonly is a glass door locked with a key (you can look inside, but you can’t touch).


DOM Navigation

  • CSS Selector: Child Combinator (>) This is a powerful selection tool.

    • div p (Descendant Selector - the Space): Analogy: "Find all p elements that are descendants of a div". This includes children, grandchildren, great-grandchildren... anyone who lives inside the div.
    • div > p (Child Selector - the >): Analogy: "Find all p elements that are direct (immediate) children of a div". This ignores grandchildren. It’s much more specific.

    Why use it? It prevents your style or script from "bleeding" into deeper nested components. It gives you surgical control over selection.





23. Events - Listening and Reacting

Events are your application’s nervous system. Every click, movement, key press is a signal you can intercept to trigger logic. Without events, the DOM is just a static sculpture. With events, it becomes an interactive robot.

onclick vs addEventListener - Two Philosophies

There are two ways to "connect a wire" to a sensor (like a button).

  • onclick (The Old Way / The Single Phone Line) This is an attribute (or property) of the element. It’s simple and direct.

    const button = document.querySelector("#my-button");

    button.onclick = function() {
    console.log("Clicked!");
    };

    The Problem: It’s like a single phone line. You can connect only one function to onclick. If you try to connect another one, the second overwrites the first.

  • addEventListener (The Modern Way / The Switchboard) This is a method. It’s like a phone switchboard: you can "add" (add) multiple "listeners" (Listener) to the same event (click).

    // Add the first listener
    button.addEventListener("click", function() {
    console.log("First handler: saving data.");
    });

    // Add a second listener
    button.addEventListener("click", function() {
    console.log("Second handler: sending analytics.");
    });

    Both are executed! It’s cleaner, more flexible, and it allows you to remove (removeEventListener) a specific handler without touching the others. Rule: Always use addEventListener. It’s the professional best practice.


Reference (fn) vs Execution (fn())

This is the most common and critical conceptual error to understand.

Analogy: Imagine you have to give an instruction to an assistant.

  • Reference (myFunction): button.addEventListener("click", myFunction); This is like giving the assistant an instruction manual (the function myFunction) and telling them: "When the phone rings (the click event), then read and execute the instructions in this manual." You’re passing the recipe, not the cake.

  • Execution (myFunction()): button.addEventListener("click", myFunction()); // WRONG ❌ This is like baking the cake immediately, giving the cake (the function’s result, which is often undefined) to the assistant and telling them: "When the phone rings, give the customer this cake". JavaScript executes myFunction() immediately (only once, when the page loads) and assigns its result (undefined) to the listener. The click will never do anything.

Rule: When you assign an event handler, you must pass the function name (the reference), not the result of executing it.


The event Object

When an event fires (the user clicks), the browser doesn’t just run your function. It runs it by automatically passing a special object as the first argument. Think of this object (e or event) as the detailed incident report.

button.addEventListener("click", function(event) { // Here it is!
console.log(event); // Inspect it! It’s full of information

// Useful information:
console.log(event.type); // "click"
console.log(event.timeStamp); // When it happened
console.log(event.ctrlKey); // Was Ctrl pressed while clicking?
console.log(event.clientX, event.clientY); // Where they clicked
});

e.target

This is the most important property of the event object. Analogy: If the event object is the fire alarm report, e.target is the specific sensor that was triggered. It’s a direct reference to the HTML element that originated the event.

// 'e.target' is the exact button that was clicked
container.addEventListener("click", function(e) {
console.log(e.target); // Shows the clicked HTML element
});

e.preventDefault()

This is your emergency brake to stop the browser’s default behavior. Certain HTML elements have a default "job":

  • Clicking a link (<a>) changes page.
  • Pressing "Enter" on a form (<form>) reloads the page.

e.preventDefault() tells the browser: "Thanks, but don’t do it. Let me handle it (JavaScript)."

const myForm = document.querySelector("#my-form");

myForm.addEventListener("submit", function(e) {
// 1. STOP THE PAGE REFRESH!
e.preventDefault();

// 2. Now I can grab the data with JS without the page disappearing
const name = document.querySelector("#name").value;
console.log(`Hi, ${name}!`);
});

Event Delegation - The Smart Listener

This is a fundamental pattern for performance.

  • The Problem: Imagine you have a list (a <ul>) with 1000 elements (<li>). Do you need to add a listener to each one? That would be a performance nightmare (1000 wires connected) and it wouldn’t work for new <li> elements added later.
  • The Solution: Leverage "Bubbling" (events "rise up" like bubbles). Put a single listener on the parent (<ul>).
  • Analogy: Instead of putting a bodyguard on every person in a room (<li>), you put one bouncer at the single exit door (<ul>).
const list = document.querySelector("#shopping-list");

// One single listener on the parent!
list.addEventListener("click", function(e) {

// 'e.target' is the sensor that triggered (the clicked <li>)

// We ask the "bouncer":
// "Is the clicked element (e.target) an LI?"
if (e.target.tagName === "LI") {
console.log("You clicked on:", e.target.textContent);
e.target.classList.toggle("bought");
}
});

Advantages:

  1. Performance: One listener instead of 1000.
  2. Dynamism: It works automatically even for new <li> elements you add in the future!

The Context Problem: this and addEventListener

This is an advanced problem that hits when you use Classes (OOP). this is a "chameleon" keyword — its meaning changes depending on who is calling the function.

  • The Problem: When addEventListener runs your function (e.g. this.clearCart), it runs it (the button’s listener). As a result, inside the function, this will no longer be your cart, but will become the button itself!
class Cart {
constructor() {
this.items = ["apple"];
const btn = document.querySelector("#clear");

// WRONG ❌
btn.addEventListener("click", this.clearCart);
}

clearCart() {
console.log(this); // 'this' here will be the <button>, not the Cart!
// this.items.length = 0; // CRASH! The button has no 'items'.
}
}

Solution: .bind(this)

.bind() is like creating a "clone" of your function with this permanently locked. Analogy: It’s like giving your assistant (the listener) an instruction manual (the function) with a name tag with your name (this) glued on top with super glue. No matter who reads that manual, it will always know that "I" refers to you (the Cart instance).

// ...inside the constructor...
// CORRECT ✅
btn.addEventListener("click", this.clearCart.bind(this));

this.clearCart.bind(this) creates a new function that, when called, will have this set correctly to the Cart.

Solution: Arrow Function Wrapper

This is the most modern and often preferred way. Arrow Functions (=>) don’t have their own this! They "inherit" the this of the place where they were written.

Analogy: It’s like you (the constructor) telling your assistant: "When the phone rings, don’t call the warehouse directly. Call me, and then I will call the warehouse."

// ...inside the constructor...
// CORRECT (and modern) ✅
btn.addEventListener("click", () => {
// This is an arrow function
// The 'this' in here is the same 'this' as the constructor
// (i.e., the Cart instance)
this.clearCart();
});

Special Events

  • keydown vs keypress (Deprecated) vs keyup Analogy: The Typewriter

    • keydown (The Key Goes Down): The moment your finger physically presses the key down. Detects ALL keys (Arrows, Shift, Ctrl, 'a'). Use this for games and controls.
    • keyup (The Key Comes Up): The moment you release the key.
    • keypress (The Character Appears): The moment the press produces a character on the paper. Ignores Arrows, Shift, Ctrl. IT’S DEPRECATED (obsolete), don’t use it.
  • change vs input (UX Feedback) This is a crucial distinction for User Experience (UX).

    • change (When you’re "done"): Fires only when the user confirms the change (usually by clicking outside the input box or pressing Enter).

      • Use: Final validation, when the user has finished typing.
    • input (Real-time): Fires on every single key press. If the user types "Hi", the event fires 2 times.

      • Use: Instant search bars, character counters, live filters.
  • submit This event fires only on the <form> element, when the user tries to submit it (pressing Enter or clicking a <button type="submit">). This is where you must use e.preventDefault().

  • DOMContentLoaded (See Section 9) The event that fires when the HTML has been built, but before images are loaded. It’s the standard "go" signal for your code.


Browser Interactions

  • confirm() (Blocking Dialog) This is not an event, but a function that creates an interaction event. confirm("Are you sure?") shows a native (and kind of ugly) dialog box that blocks execution of all JavaScript until the user makes a choice.

    console.log("Before confirmation");

    const userConfirmed = confirm("Do you really want to delete everything?");
    // The code stops here and waits...

    if (userConfirmed) { // 'confirm' returns true (OK) or false (Cancel)
    console.log("Deletion in progress...");
    } else {
    console.log("Cancelled.");
    }

    Warning: It blocks the entire interface. Modern apps avoid confirm and use custom modals (like <dialog>) for a better experience.





24. Dialogs and Modals

For decades, to show a "pop-up" (a modal), developers had to fight with absolutely positioned divs, z-index, semi-transparent background divs (the backdrop), and complicated JavaScript logic to "trap" keyboard focus. It was a nightmare for accessibility and maintenance.

The <dialog> element is the browser’s native and modern solution to this problem. It’s as if the browser provides you with a pre-built portable stage.

<dialog> (The Stage Element)

Think of the <dialog> element as a stage you keep backstage (hidden) in your theater (the web page).

<dialog id="my-dialog">
<h2>Modal Title</h2>
<p>This is an important message.</p>
<button id="close-btn">Close</button>
<form method="dialog">
<button value="confirmed">Confirm</button>
<button value="cancelled">Cancel</button>
</form>
</dialog>

<button id="open-btn">Open Modal</button>

By default, this element does nothing and is invisible. To make it "go on stage", you have two JavaScript methods, and this is where the magic happens.

showModal() (Modal) vs show() (Non-Modal)

This is the fundamental distinction. You’re bringing the actor on stage, but how?

  • showModal() (The Stage Spotlight 🔦) This is the method you’ll want to use 99% of the time. It’s the "real" modal.

    Analogy: It’s like turning on a spotlight on the actor (<dialog>) and dimming the lights on everything else on the page (the backdrop).

    const dialog = document.querySelector("#my-dialog");
    const openBtn = document.querySelector("#open-btn");

    openBtn.addEventListener("click", () => {
    dialog.showModal();
    });

    What showModal() automatically does for you:

    1. Creates a Backdrop: Adds a semi-transparent background (::backdrop) that covers the rest of the page.
    2. Blocks Interaction: The user cannot click or interact with anything else on the page (links, buttons, etc.) until the modal is closed.
    3. Traps Focus (Focus Trap): If the user presses Tab, keyboard focus stays trapped inside the modal, cycling only through its interactive elements. This is a fundamental requirement for accessibility (a11y).
    4. Closes with Esc: The user can close the modal simply by pressing the Esc key.
  • show() (The Info Panel 📰) This method is for a "non-modal" or "dialog".

    Analogy: It’s like an info panel or a subtitle that appears in a corner of the screen (like a chat or a cookie notice). You can still interact with the rest of the page while it’s visible.

    // Less common
    dialog.show();

    What show() does NOT do:

    1. No backdrop.
    2. No interaction blocking.
    3. No focus trap.
    4. The Esc key does not close it.

Rule: Use showModal() for anything that requires the user’s exclusive attention (confirmations, forms, alerts). Use show() for non-intrusive notifications (rare).


close() (Exiting the Stage)

This is the method to programmatically close the dialog.

Analogy: It’s the actor taking a bow and exiting the stage.

const dialog = document.querySelector("#my-dialog");
const closeBtn = document.querySelector("#close-btn");

// Method 1: Close with JavaScript
closeBtn.addEventListener("click", () => {
dialog.close(); // Closes the dialog
});

// Method 2: The Form trick (cleaner!)
// Any <button> inside a <form method="dialog">
// will automatically close the dialog when clicked.
// No JavaScript needed!

The Superpower: returnValue When you close a dialog, you can optionally pass a "closing message" (a returnValue).

Analogy: It’s as if the actor, when exiting the stage, hands you a note saying what they decided ("confirmed" or "cancelled").

<form method="dialog">
<button value="confirmed">Confirm</button>
<button value="cancelled">Cancel</button>
</form>
// JS to "read" the note
const dialog = document.querySelector("#my-dialog");

// Listen to the 'close' event
dialog.addEventListener("close", () => {
// Read the note!
console.log("The dialog closed. Returned value:", dialog.returnValue);

if (dialog.returnValue === "confirmed") {
console.log("The user confirmed!");
// ...run the confirmation logic...
} else {
console.log("The user cancelled.");
}
});

If the user presses Esc, the returnValue will be an empty string "" (or the value of a possible cancel button).

The <dialog> element eliminates 90% of the dirty work and bugs that plagued "hand-made" modals, handling focus, backdrop, and closing in a native and accessible way.













Asynchrony and Server Communication

25. The Concept - Synchronous vs Asynchronous

What it does: Defines how code is executed over time.

The problem: JavaScript is "single-threaded" (it has only one hand). If you make it do a long calculation synchronously, you block the entire site until it finishes.

Analogy: The Toaster 🍞

  • Synchronous (Blocking): You put the bread in, and you stay still staring at the toaster for 2 minutes until it pops. You can’t do anything else. (The site freezes).
  • Asynchronous (Non-blocking): You put the bread in, push the lever, and meanwhile you go grab the milk and coffee. When the toaster goes DING! (callback/promise), you come back to get the bread. (The site stays smooth).




26. fetch() - Ordering at a restaurant

What it does: Requests data from an external server (e.g. weather, users, products).

The secret: fetch is a two-phase process. You don’t get the data right away, you get a promise that you’ll receive it!

fetch('https://api.example.com/pizza')
.then(res => res.json()) // 1. Open the box (Parsing)
.then(data => console.log(data)) // 2. Eat the contents (Use the data)
.catch(err => console.error(err)); // Error handling

Analogy: The double order at fast food 🍔

  1. First step (The Network): You order and they give you a pager. When it buzzes, they hand you the tray with a closed box (res). You still can’t see the burger, you only know the box arrived (or if the kitchen exploded 404/500).
  2. Second step (Parsing): You have to open the box and unwrap the burger. It takes time! This is res.json(). When you’re done, you finally have the real food (data).




27. async / await - Automatic transmission

What it does: The modern way to handle waiting. It makes asynchronous (complex) code look like synchronous (linear and simple) code.

They are not inseparable. It is often thought that they must go together, but they do two distinct jobs:

  • async: It's the Authorization. It declares to JavaScript: "Things that take time might happen in this function, be ready".
  • await: It's the actual Pause Button. It says: "Stop on this exact line and don't proceed until you receive the data".

Remember:

  • You can have async without await: the function will run extremely fast without ever pausing (syntactically valid, but useless).
  • You can NEVER have await without async: if you try to press the "pause button" in a regular function, JavaScript panics and throws a fatal error. await requires the async authorization.
// Before: Manual transmission (.then .then) 😫
fetch(url).then(res => res.json()).then(data => ...);

// After: Automatic transmission (async/await) 😎
async function fetchData() {
    // "await" pauses the function until the promise resolves
    const res = await fetch(url);      // Wait for the box
    const data = await res.json();     // Wait to open it
    console.log(data);                 // Done!
}

Analogy:

  • .then() is like driving with manual transmission: you have to manage every gear, clutch, and data handoff. Powerful, but tiring.
  • await is automatic transmission: you press the accelerator and the engine handles pauses and state changes for you.




28. try...catch - The Trapeze Artist

What it does: The universal safety net. It catches any error, whether it depends on the unpredictability of the real world (the user enters a tunnel and loses 4G, the server malfunctions), or it derives from a typo on your end (synchronous).

async function operation() {
    try {
        // The trapeze artist jumps...
        const res = await fetch(url);
        const data = await res.json();
        
        // Even if you mess up here, catch will handle it!
        const name = data.user.toUpperCase(); 
    } catch (err) {
        // ...the net catches them if the server crashes or if you mess up!
        console.error("Technical error:", err); // For you
        alert("Unable to load data");           // For the user
    }
}

Analogy:

  • try { ... }: It’s the trapeze artist attempting a triple somersault.
  • catch { ... }: It’s the net underneath. If the trapeze artist slips (fetch fails) or gets a cramp (a bug in the code), they land safely in the net instead of smashing into the ground (crashing the app).

Golden rule: Handle two levels of errors!

  1. Console (Developer): The engine warning light 🔧 (console.error with technical details to understand if it's your fault or the server's).
  2. UI (User): The dashboard warning light 💡 (A friendly message: "There was a connection problem, try again").












Data Persistence and Storage

29. localStorage - Long-Term Memory

So far, all our variables (let, const) have lived in the browser’s RAM. They’re like notes written on a whiteboard: fast, useful, but as soon as the user refreshes or closes the page (F5), the board gets wiped clean. Everything disappears.

localStorage is a completely different technology. It’s your website’s long-term memory.

Analogy: It’s not a whiteboard, it’s a desk drawer (or a safe) built into the user’s browser. The data you put in here (settings, scores, a shopping cart) survives refreshes, browser closes, and even shutting down the computer. When the user comes back to your site a week later, the data is still there waiting.

The Fundamental Problem: The String Wall

There’s a golden rule, a “but” as big as a house, that you must never forget: localStorage can store only and exclusively text strings.

Think of localStorage as an old fax machine. A fax can’t send a package (an object), a deck of cards (an array), or a switch (a boolean). It can only send a flat sheet of paper (a string).

This is the mistake everyone makes at the beginning:

// This is what you would like to do
const user = {
name: "Alice",
level: 12,
isPremium: true
};

// COMMON ERROR: Saving directly ❌
localStorage.setItem('user', user);

// What did you actually save?
const saved = localStorage.getItem('user');
console.log(saved); // Output: "[object Object]"

You lost everything! The name, the level, everything got squashed into that useless string, "[object Object]". It’s like trying to fax a package: on the other side, only a black, unreadable sheet arrived.


JSON - The Universal Data Translator

How do we solve the fax problem? We don’t send the package; we send a document that describes what’s inside the package, piece by piece.

JSON (JavaScript Object Notation) is the universal language for describing complex data (objects, arrays) in a flat text format (a string).

Analogy: JSON is the IKEA instruction manual. It takes a complex piece of furniture (your object) and “flattens” it into a manual (the string). Anyone who receives the manual can “rebuild” an identical piece of furniture.

There are two main commands you need to know:

JSON.stringify() (The Disassembler/Flattener)

JSON.stringify() (literally “string-ifies”, turns into a string) is the “disassembly” process. It takes your live JavaScript object or array and converts it into a JSON text string that describes it perfectly.

Analogy: It’s the machine that disassembles IKEA furniture and packs it into the flat box.

const settings = {
theme: 'dark',
notifications: true,
volume: 80,
sounds: {
click: true,
notification: false
}
};

// The magic of "disassembly"
const jsonString = JSON.stringify(settings);

console.log(jsonString);
// Output (a single, long text string):
// '{"theme":"dark","notifications":true,"volume":80,"sounds":{"click":true,"notification":false}}'

// NOW you can save it! It’s just text!
localStorage.setItem('settings', jsonString);

JSON.parse() (The Reassembler/Rebuilder)

JSON.parse() is the reverse operation. It takes a JSON text string (the “manual”) and “parses” it, rebuilding the original JavaScript object or array.

Analogy: It’s the act of following the IKEA instructions to rebuild the furniture.

// Retrieve the string from localStorage (the "manual")
const savedString = localStorage.getItem('settings');
console.log(typeof savedString); // "string" - it’s still just text!

// The magic of "reassembly"
const rebuiltSettings = JSON.parse(savedString);
console.log(typeof rebuiltSettings); // "object" - it’s an object again!

// Now you can use it like a normal JavaScript object
if (rebuiltSettings.theme === 'dark') {
document.body.classList.add('dark-mode');
}
console.log(rebuiltSettings.volume); // 80

The Full Data Lifecycle

This is the flow you will always use:

// PHASE 1: CREATION - You have your JavaScript data (an array)
const todoList = [
{ id: 1, text: "Learn localStorage" },
{ id: 2, text: "Conquer the world" }
];

// PHASE 2: SERIALIZATION (Disassembly) - Turn into a string
const todoListString = JSON.stringify(todoList);

// PHASE 3: SAVING (setItem) - Put the string in the "drawer"
localStorage.setItem('todos', todoListString);

// ... The user closes the browser, shuts down the PC, goes to sleep ...
// ... The next day they reopen your site ...

// PHASE 4: RETRIEVAL (getItem) - Read from the "drawer"
const retrievedTodoList = localStorage.getItem('todos');
// It’s still a string! '[{"id":1,...}, ...]'

// PHASE 5: DESERIALIZATION (Reassembly) - Turn back into an object
const rebuiltTodoList = JSON.parse(retrievedTodoList);

// PHASE 6: USE - Work with the data as usual
console.log(rebuiltTodoList[0].text); // "Learn localStorage"

Handling First Run - The Robust Pattern

What happens when a user visits your site for the first time? The “drawer” is empty. localStorage.getItem('todos') will return null.

If you do JSON.parse(null), the result is null (not an error). But if you then try .push()... CRASH.

You must always provide a default value (a “Plan B”).

  • The || (OR) Pattern - The Most Common This is the quickest and most concise way, leveraging “falsy” values.

    // Try to parse saved data.
    // If `getItem` returns `null`, `JSON.parse(null)` returns `null`.
    // `null` is falsy, so the OR operator (||) chooses the "Plan B": an empty array.
    const tasks = JSON.parse(localStorage.getItem("data")) || [];
  • The try-catch Pattern - The Safest What if the data in localStorage is corrupted? (e.g., a badly written JSON string: "{name: 'mario'}"). In this case, JSON.parse() will throw an error and crash your app. try-catch is the safety net that prevents this.

    function loadSafeData(key, defaultValue) {
    try {
    const item = localStorage.getItem(key);
    // If 'item' is null, the ternary returns the default.
    // If 'item' exists, try to parse it.
    return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
    // If parsing fails (corrupted data),
    // log the error and return the default.
    console.error(`Error parsing ${key} from localStorage:`, error);
    return defaultValue;
    }
    }

    // Usage:
    const tasks = loadSafeData('data', []);

Inspecting localStorage in DevTools

You don’t have to work blind! The browser gives you a secret window to look inside the localStorage drawer.

Analogy: It’s like having X-ray vision on the user’s desk.

How to access it (in Chrome/Edge/Firefox):

  1. Open DevTools: F12 (or right click > Inspect)

  2. Find the Storage section:

    • Chrome/Edge: "Application" tab → Storage → Local Storage
    • Firefox: "Storage" tab → Local Storage
  3. Click your domain (e.g., http://127.0.0.1:5500)

You’ll see a very simple table:

KeyValue
settings{"theme":"dark","notifications":true,...}
todos[{"id":1,"text":"..."},...]

From here you can verify, edit values on the fly (double click), delete individual keys (Delete) or clear everything (trash icon). It’s the #1 tool for debugging persistence.


localStorage Limits - The Rules of the Game

localStorage is great, but it’s not an infinite database. It has specific rules and limits:

  1. Strings only: (We already said it, but it’s that important).

  2. Limited space (about 5-10 MB): Analogy: It’s a drawer, not a warehouse. It’s perfect for settings, a cart, a username. It’s terrible for saving photos, audio files, or thousands of records.

  3. Synchronous (Blocking): When you call localStorage.setItem('stuff', '...'), your JavaScript stops and waits for the operation to be written to disk. If you save 1KB, it’s instant. If you try to save a 4MB string, your page will freeze for a fraction of a second. Use it for small, fast data.

  4. No security (It’s plain text!): Analogy: It’s a post-it note stuck to the screen, not a safe. Anyone with physical access to the user’s browser (or via an XSS attack) can open DevTools and read everything inside. NEVER SAVE:

    • Passwords
    • API tokens (unless strictly necessary)
    • Credit card numbers
    • Any sensitive data.

Best Practices and Advanced Patterns

  • Versioning: What happens if v2 of your app changes the data structure (e.g., from user: "Mario" to user: { name: "Mario" })? You must handle migration.

    const APP_VERSION = "v2";
    const savedVersion = localStorage.getItem("appVersion");

    if (savedVersion !== APP_VERSION) {
    // Migration logic... (e.g., load old data,
    // transform it, save it in the new format)
    localStorage.setItem("appVersion", APP_VERSION);
    }
  • Expiration (TTL - Time To Live): localStorage has no expiration date. Data stays there forever. If you want it to expire (e.g., a login), you must build it yourself:

    function setWithExpiry(key, value, ttl_ms) {
    const item = {
    value: value,
    expiry: Date.now() + ttl_ms // Save the expiry timestamp
    };
    localStorage.setItem(key, JSON.stringify(item));
    }
    // ...and a get() function that checks the timestamp...
  • Namespace (Prefixes): If multiple scripts (or different apps on the same domain) use localStorage, they could overwrite each other (e.g., both use the key user). Analogy: It’s like labeling your moving boxes with "Apt. 12B" so you don’t mix them up with the ones from "Apt. 10A".

    const APP_PREFIX = "myApp_";
    localStorage.setItem(APP_PREFIX + 'user', '...');
    localStorage.setItem(APP_PREFIX + 'settings', '...');

The localStorage Commandments 📜

  1. You will save only strings (Remember JSON.stringify()).
  2. You will parse with caution (Remember JSON.parse()).
  3. You will never trust blindly (Handle first-run null with || []).
  4. You will protect against crashes (Use try-catch for corrupted data).
  5. You will not save sensitive data (It’s a post-it note, not a safe).
  6. You will respect the limits (Keep data small, under 5MB).
  7. You will avoid blocking (Don’t save huge data synchronously).
  8. You will inspect in DevTools (Don’t work blind).
  9. You will use a prefix (Namespace) (Avoid conflicts with other apps).
  10. You will handle versions (Prepare your code to migrate future data).