Fejezetek: 1. Aszinkron 2. Böngésző API-k 3. Biztonság
2. fejezet

Főbb böngésző API-k

Fetch, DOM manipuláció, eseménykezelés, Web Storage, időzítők és további böngésző API-k – a kliens oldali fejlesztés eszköztára.

2.1 Fetch API

Mi az a Fetch API?

A fetch() a modern böngészők beépített HTTP kliens API-ja, amely felváltotta a régebbi XMLHttpRequest-et. A legnagyobb előnye, hogy Promise-alapú – az 1. fejezetben tanult async/await mintával természetesen használható. A fetch() globális függvény, tehát bárhol meghívható JavaScript kódból.

A Fetch API kétlépcsős: először a válaszfejléceket kapjuk meg (Response objektum), majd egy második aszinkron lépésben a válasz törzsét (body) kell kiolvassuk. Ez azért hasznos, mert a fejlécekből (pl. státuszkód) már eldönthetjük, hogy egyáltalán érdemes-e a törzsöt feldolgozni.

Alap szintaxis: GET kérés

async function getUsers() {
  // 1. lépés: a fetch() elindítja a kérést, és Promise-t ad vissza.
  //    Az await megvárja, amíg a FEJLÉCEK megérkeznek.
  var response = await fetch("/api/users");

  // 2. lépés: a response.ok ellenőrzi, hogy a státuszkód 200-299 közé esik-e.
  //    FONTOS: a fetch() NEM dob hibát 404-nél vagy 500-nál!
  if (!response.ok) {
    throw new Error("HTTP hiba: " + response.status);
  }

  // 3. lépés: a válasz törzsének kiolvasása JSON-ként.
  //    Ez is aszinkron művelet (a törzs streamelve érkezik).
  var data = await response.json();

  return data;
}
⚠ Figyelem – a fetch() NEM dob hibát HTTP hibakódnál!
Ez a leggyakoribb kezdő hiba! A fetch() Promise-e csak hálózati hibánál (nincs internet, DNS hiba, szerver nem elérhető) lesz rejected. Ha a szerver válaszol – akár 404-gyel, akár 500-zal –, a Promise FULFILLED lesz! Ezért MINDIG ellenőrizzük a response.ok tulajdonságot (vagy a response.status értékét).

A Response objektum fontosabb tulajdonságai

Tulajdonság / MetódusLeírásPélda
response.oktrue, ha a státuszkód 200–299if (!response.ok) throw ...
response.statusHTTP státuszkód (szám)200, 404, 500
response.statusTextStátusz szöveg"OK", "Not Found"
response.headersVálaszfejlécek (Headers objektum)response.headers.get("Content-Type")
response.json()Törzs kiolvasása JSON-kéntA leggyakoribb API válasz
response.text()Törzs kiolvasása nyers szövegkéntHTML, XML, CSV válasz
response.blob()Törzs kiolvasása bináriskéntKép, PDF letöltés

HTTP metódusok: POST, PUT, DELETE

Az alapértelmezett metódus GET. Más metódusokhoz a fetch() második paramétereként egy konfigurációs objektumot adunk meg, amelyben meghatározzuk a metódust, a fejléceket és a kérés törzsét (body).

// POST kérés – új erőforrás létrehozása
// A Content-Type fejléccel jelezzük, hogy JSON-t küldünk.
// A body-t JSON.stringify()-jal alakítjuk szöveggé.
async function createUser(userData) {
  var response = await fetch("/api/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(userData),
  });

  if (!response.ok) throw new Error("HTTP " + response.status);
  return await response.json();  // a szerver visszaadja a létrehozott user-t
}

// PUT kérés – meglévő erőforrás teljes frissítése
async function updateUser(id, userData) {
  var response = await fetch("/api/users/" + id, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData),
  });
  if (!response.ok) throw new Error("HTTP " + response.status);
  return await response.json();
}

// DELETE kérés – erőforrás törlése
// A DELETE-nek általában nincs body-ja.
async function deleteUser(id) {
  var response = await fetch("/api/users/" + id, {
    method: "DELETE",
  });
  return response.ok;  // true ha sikeres (200–299)
}

AbortController – kérés megszakítása

Előfordul, hogy egy kérést meg akarunk szakítani – például ha a felhasználó elnavigál az oldalról, vagy ha időkorlátot (timeout) szeretnénk beállítani. Az AbortController API-val jelzést (signal) küldhetünk a fetch-nek, hogy álljon le.

var controller = new AbortController();

// 5 mp timeout: ha a szerver nem válaszol, megszakítjuk
var timeoutId = setTimeout(function() {
  controller.abort();
}, 5000);

try {
  var response = await fetch("/api/slow-endpoint", {
    signal: controller.signal,  // a fetch figyeli ezt a jelzést
  });
  clearTimeout(timeoutId);  // sikeres válasz => töröljük a timeout-ot
  var data = await response.json();
} catch (err) {
  if (err.name === "AbortError") {
    console.log("A kérés megszakítva (timeout vagy felhasználó).");
  } else {
    console.error("Hálózati hiba:", err);
  }
}
ℹ Megjegyzés
Az AbortController nem csak fetch-hez használható – bármilyen aszinkron művelet jelzésére alkalmas. Egy controller.signal-t több fetch-hez is hozzárendelhetünk, így egyetlen abort() hívással az összeset leállíthatjuk.

Gyakorlati API wrapper

Nagyobb alkalmazásokban nem akarjuk minden egyes fetch hívásnál megismételni a hibaellenőrzést, a fejlécek beállítását és a JSON feldolgozást. Ehelyett készítünk egy újrafelhasználható wrapper objektumot:

var api = {
  baseUrl: "/api",

  // Az alap request metódus, amely minden közös logikát tartalmaz
  request: async function(endpoint, options) {
    var url = this.baseUrl + endpoint;
    var defaults = {
      headers: { "Content-Type": "application/json" },
    };
    var config = Object.assign({}, defaults, options);

    var response = await fetch(url, config);

    if (!response.ok) {
      // Megpróbáljuk kiolvasni a szerver hibaüzenetét
      var errorBody = await response.text().catch(function() { return ""; });
      throw new Error("HTTP " + response.status + ": " + errorBody);
    }

    return await response.json();
  },

  // Kényelmi metódusok
  get: function(endpoint) {
    return this.request(endpoint);
  },

  post: function(endpoint, data) {
    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify(data),
    });
  },

  put: function(endpoint, data) {
    return this.request(endpoint, {
      method: "PUT",
      body: JSON.stringify(data),
    });
  },

  del: function(endpoint) {
    return this.request(endpoint, { method: "DELETE" });
  },
};

// Használat – tiszta és tömör:
var users = await api.get("/users");
var newUser = await api.post("/users", { name: "Anna", age: 28 });
await api.put("/users/42", { name: "Anna", age: 29 });
await api.del("/users/42");
✔ Tipp
A wrapper előnye, hogy a hibaellenőrzés, a base URL kezelés és a JSON fejlécek EGY helyen vannak. Ha a szerver autentikációt kér (pl. Bearer token), elég egyetlen helyen módosítani a request metódust.

2.2 REST API alapismeretek

Mi az a REST?

A REST (Representational State Transfer) egy architektúrális stílus, amely szabályokat ad arra, hogyan kommunikáljanak a kliens és a szerver alkalmazások HTTP-n keresztül. A REST nem protokoll és nem szabvány – inkább konvenciók és elvek gyűjteménye, amelyet a legtöbb modern web API követ.

A REST lényege: az erőforrásokat (felhasználók, termékek, rendelések) URL-ekkel azonosítjuk, és a HTTP metódusokkal (GET, POST, PUT, DELETE) végzünk rajtuk műveleteket. A kommunikáció állapotmentes (stateless) – minden kérésnek tartalmaznia kell az összes szükséges információt, a szerver nem emlékszik a korábbi kérésekre.

Erőforrások és URL-ek

A REST API-ban minden URL egy erőforrást (resource) jelöl. Az URL-ek konvenció szerint főnevek (nem igék!), és többes számúak:

URLErőforrásLeírás
/api/usersFelhasználók gyűjteményAz összes felhasználó
/api/users/42Egyetlen felhasználóA 42-es ID-jú felhasználó
/api/users/42/ordersBeágyazott erőforrásA 42-es user rendelései
/api/products?category=electronicsSzűrt gyűjteményElektronikai termékek
✘ Rossz URL-ek – igék az URL-ben
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser/42
✔ Helyes URL-ek – főnevek, többes szám
GET     /api/users — listázás
POST    /api/users — létrehozás
GET     /api/users/42 — egy elem lekérése
PUT     /api/users/42 — teljes frissítés
PATCH   /api/users/42 — részleges frissítés
DELETE  /api/users/42 — törlés

HTTP metódusok és CRUD műveletek

A REST API-ban a HTTP metódusok felelnek meg a CRUD (Create, Read, Update, Delete) műveleteknek. Fontos fogalom az idempotencia: egy művelet idempotens, ha többször végrehajtva ugyanazt az eredményt adja.

HTTP metódusCRUDIdempotens?Body?Példa
GETReadIgenNemFelhasználók listázása
POSTCreateNemIgenÚj felhasználó regisztráció
PUTUpdate (teljes)IgenIgenProfil összes mezőjének frissítése
PATCHUpdate (részleges)NemIgenCsak az e-mail cím módosítása
DELETEDeleteIgenNemFelhasználó törlése
ℹ Idempotens – mit jelent a gyakorlatban?
A GET, PUT és DELETE idempotens: ha kétszer törlünk egy erőforrást, a második hívás sem okoz problémát (már nincs mit törölni – a szerver 404-et ad). A POST NEM idempotens: ha kétszer hívjuk meg, két új erőforrás jön létre. Ezért fontos, hogy a „Megrendelés" gombot letiltsuk az első kattintás után!

HTTP státuszkódok

A szerver a válasz státuszkódjával jelzi a művelet eredményét. A kódok százas csoportokba tartoznak: 2xx = siker, 4xx = kliens hiba, 5xx = szerver hiba.

KódJelentésMikor kapjuk?
200 OKSikeres kérésGET, PUT, PATCH sikeres válasz
201 CreatedErőforrás létrehozvaSikeres POST (új elem létrejött)
204 No ContentSikeres, nincs válasz bodySikeres DELETE
400 Bad RequestHibás kérésValidációs hiba, hiányzó mezők
401 UnauthorizedNincs hitelesítésHiányzó vagy lejárt token
403 ForbiddenNincs jogosultságHitelesített, de nincs joga hozzá
404 Not FoundNem találhatóNem létező erőforrás (/users/99999)
409 ConflictÜtközésDuplikált e-mail vagy felhasználónév
500 Internal ErrorSzerverhibaVáratlan szerver oldali hiba

Teljes CRUD példa Fetch API-val

Az alábbi példa bemutatja, hogyan valósítjuk meg a CRUD műveleteket a 2.1-ben tanult Fetch API-val. Figyeljük meg, hogy a HTTP metódus és az URL együtt határozza meg a műveletet:

// ── CREATE: új felhasználó létrehozása ──
async function createUser(userData) {
  var response = await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData),
  });

  if (response.status === 201) {
    return await response.json();  // A szerver visszaadja az új user-t (id-vel)
  }
  if (response.status === 400) {
    var errors = await response.json();
    throw new Error("Validáció: " + errors.message);
  }
  throw new Error("HTTP " + response.status);
}

// ── READ: összes felhasználó lekérdezése (lapozással) ──
async function getUsers(page, limit) {
  var url = "/api/users?page=" + page + "&limit=" + limit;
  var response = await fetch(url);

  if (!response.ok) throw new Error("HTTP " + response.status);
  return await response.json();
  // Válasz: { data: [...], total: 150, page: 1, limit: 20 }
}

// ── READ: egyetlen felhasználó ID alapján ──
async function getUser(id) {
  var response = await fetch("/api/users/" + id);

  if (response.status === 404) return null;  // Nem létezik
  if (!response.ok) throw new Error("HTTP " + response.status);
  return await response.json();
}

// ── UPDATE: felhasználó teljes frissítése ──
async function updateUser(id, userData) {
  var response = await fetch("/api/users/" + id, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData),
  });

  if (!response.ok) throw new Error("HTTP " + response.status);
  return await response.json();
}

// ── DELETE: felhasználó törlése ──
async function deleteUser(id) {
  var response = await fetch("/api/users/" + id, {
    method: "DELETE",
  });

  if (response.status === 404) return false; // Már nem létezik
  if (!response.ok) throw new Error("HTTP " + response.status);
  return true;
}

JSON válasz formátumok

A legtöbb REST API JSON formátumban kommunikál. Érdemes egységes válaszstruktúrát használni – így a kliens oldali kód mindig tudja, mit várjon:

Sikeres válasz – egyetlen elem (GET /api/users/42200 OK)

{
  "id": 42,
  "name": "János",
  "email": "janos@example.com",
  "createdAt": "2025-01-15T10:30:00Z"
}

Sikeres válasz – lista lapozással (GET /api/users?page=2&limit=10200 OK)

{
  "data": [
    { "id": 11, "name": "Anna", "email": "anna@example.com" },
    { "id": 12, "name": "Béla", "email": "bela@example.com" }
  ],
  "total": 150,
  "page": 2,
  "limit": 10,
  "totalPages": 15
}

Hiba válasz (POST /api/users400 Bad Request)

400
{
  "error": "Validation failed",
  "message": "Hiányos adatok.",
  "details": [
    { "field": "email", "message": "Kötelező mező" },
    { "field": "name", "message": "Minimum 2 karakter" }
  ]
}

Hibakezelés a gyakorlatban

A 2.1-es API wrapper-t kibővíthetjük, hogy a különböző státuszkódokra eltérően reagáljon. A 4xx hibák a kliens hibái (javíthatók), az 5xx hibák a szerver hibái (nem javíthatók kliensről):

async function handleResponse(response) {
  // Siker (200–299)
  if (response.ok) {
    if (response.status === 204) return null;  // No Content (pl. DELETE)
    return await response.json();
  }

  // Megpróbáljuk kiolvasni a szerver hibaüzenetét
  var errorData;
  try {
    errorData = await response.json();
  } catch (e) {
    errorData = { message: "Ismeretlen hiba" };
  }

  if (response.status === 401) {
    window.location.href = "/login";  // Lejárt token => bejelentkezés
    return;
  }

  if (response.status === 403) {
    showError("Nincs jogosultságod ehhez a művelethez.");
    return;
  }

  if (response.status === 400 || response.status === 422) {
    if (errorData.details) {
      showValidationErrors(errorData.details);
    }
    return;
  }

  throw new Error(errorData.message || "HTTP " + response.status);
}
✔ REST API tesztelés
A fejlesztés során a REST API-kat tesztelhetjük a böngésző DevTools → Network fülén (kérések és válaszok vizsgálata), valamint eszközökkel mint a Postman vagy a Thunder Client (VS Code bővítmény). Ezekkel könnyen küldhetünk GET, POST, PUT, DELETE kéréseket anélkül, hogy frontend kódot kellene írnunk.

2.3 DOM manipuláció

Mi az a DOM?

A DOM (Document Object Model) a HTML dokumentum fa-struktúrájú reprezentációja a memóriában. Amikor a böngésző betölti a HTML-t, felépíti a DOM fát, amelyet a JavaScript szabadon olvashat és módosíthat. Minden HTML elem egy csomópont (node) a fában – a <html> a gyökér, és minden más elem ennek leszármazottja.

A DOM manipuláció az a folyamat, amellyel JavaScript-ből módosítjuk az oldal tartalmát, megjelenését vagy szerkezetét – mindezt az oldal újratöltése nélkül.

Elemek kiválasztása

Mielőtt módosíthatnánk egy elemet, először meg kell találnunk a DOM fában. A két legfontosabb metódus a querySelector (egy elem) és a querySelectorAll (több elem). Mindkettő CSS szelektorokkal dolgozik, tehát ugyanazokat a szelektorokat használhatjuk, mint a CSS-ben.

MetódusVisszatérésHa nincs találatMikor használjuk?
querySelector(sel)Első egyező elemnullEgyetlen elem (szinte mindig ez kell)
querySelectorAll(sel)NodeList (összes egyező)Üres NodeListTöbb elem
getElementById(id)Elem id alapjánnullRégi kód, ma querySelector elég
closest(sel)Legközelebbi ős (felfelé)nullEseménydelegálásnál (lásd 2.3)
// Egyetlen elem kiválasztása
var title = document.querySelector("h1");          // elem típus szerint
var btn = document.querySelector("#submit-btn");    // id szerint
var card = document.querySelector(".card.active");  // összetett CSS szelektor

// FIGYELEM: ha nem létezik az elem, null-t kapunk!
var elem = document.querySelector(".nonexistent");
console.log(elem);  // null

// Több elem kiválasztása
var items = document.querySelectorAll(".list-item");
console.log(items.length);  // pl. 5

// NodeList bejárása forEach-csel
items.forEach(function(item, index) {
  console.log(index + ": " + item.textContent);
});
ℹ NodeList vs. HTMLCollection
A querySelectorAll() statikus NodeList-et ad vissza (pillanatfelvétel). A régebbi getElementsByClassName() élő HTMLCollection-t ad – ha a DOM változik, a lista automatikusan frissül. A modern fejlesztésben a querySelectorAll() az ajánlott.

Tartalom módosítása: textContent vs. innerHTML

Az elem tartalmát kétféleképpen módosíthatjuk, de a biztonsági következmények alapvetően különböznek:

✘ XSS veszély!
// innerHTML ÉRTELMEZI a HTML-t => ha felhasználói adat kerül bele,
// a böngésző végrehajtja a benne lévő scripteket!
var userInput = '<img src=x onerror="alert(document.cookie)">';
elem.innerHTML = "<p>Üdv, " + userInput + "!</p>";
// Az onerror lefut => XSS támadás! (Lásd 3.1 fejezet)
✔ Biztonságos
// textContent NEM értelmez HTML-t – nyers szövegként kezeli
var userInput = '<img src=x onerror="alert(1)">';
elem.textContent = "Üdv, " + userInput + "!";
// Kimenet: "Üdv, <img src=x onerror="alert(1)">!" (nyers szöveg)

Attribútumok kezelése

Az elemek HTML attribútumait olvashatjuk és módosíthatjuk. Különösen hasznos a data-* attribútumok rendszere, amellyel egyedi adatokat csatolhatunk az elemekhez:

// Hagyományos attribútum-kezelés
var img = document.querySelector("img");
img.setAttribute("src", "/images/photo.jpg");
img.setAttribute("alt", "Profilkép");
var src = img.getAttribute("src");

// data-* attribútumok – egyedi adatok HTML-ben
// HTML: <div class="card" data-user-id="42" data-role="admin">
var card = document.querySelector(".card");
var userId = card.dataset.userId;  // "42" (data-user-id => dataset.userId)
var role = card.dataset.role;      // "admin"

// A kötőjeles nevek camelCase-re alakulnak:
// data-user-id  => dataset.userId
// data-is-active => dataset.isActive

Osztályok kezelése: classList

Az elemek CSS osztályait a classList API-val kezeljük. Ez sokkal kényelmesebb és biztonságosabb, mint a régi className string manipuláció:

var card = document.querySelector(".card");

card.classList.add("active");           // Hozzáadás
card.classList.add("highlight", "big"); // Többet egyszerre
card.classList.remove("hidden");        // Eltávolítás
card.classList.toggle("expanded");      // Kapcsoló: ha van, leveszi; ha nincs, hozzáadja
var isActive = card.classList.contains("active"); // Lekérdezés: true/false

// Gyakori minta: aktív elem váltás
document.querySelectorAll(".tab").forEach(function(tab) {
  tab.classList.remove("active");      // Mindről levesszük
});
clickedTab.classList.add("active");    // Csak a kattintottra tesszük

Elemek létrehozása és beszúrása

Új HTML elemeket programozottan hozhatunk létre és szúrhatunk be a DOM-ba. Ez biztonságosabb, mint az innerHTML, és jobban kézben tartható:

// 1. Elem létrehozása (még NEM jelenik meg az oldalon!)
var li = document.createElement("li");

// 2. Tartalom és attribútumok beállítása
li.textContent = "Új tennivaló";
li.classList.add("todo-item");
li.dataset.id = "123";

// 3. Beszúrás a DOM-ba (itt jelenik meg az oldalon)
var list = document.querySelector("ul");
list.append(li);       // Lista VÉGÉRE
list.prepend(li);      // Lista ELEJÉRE

// Beszúrás egy adott elem elé/mögé
var ref = document.querySelector(".reference");
ref.before(li);        // A referencia ELÉ
ref.after(li);         // A referencia MÖGÉ

// Törlés
li.remove();

DocumentFragment – hatékony tömeges beszúrás

Ha sok elemet kell egyszerre beszúrni, a DocumentFragment egy „láthatatlan konténer", amely nem része a DOM-nak. Az elemeket ebbe gyűjtjük, majd egyetlen művelettel szúrjuk be – így a böngésző csak EGYSZER rajzolja újra az oldalt (reflow), nem minden egyes elemnél:

// 100 listaelem hatékony beszúrása
var fragment = document.createDocumentFragment();

for (var i = 0; i < 100; i++) {
  var li = document.createElement("li");
  li.textContent = "Elem " + (i + 1);
  fragment.append(li);  // A fragment-be gyűjtjük (NEM a DOM-ba!)
}

document.querySelector("ul").append(fragment);
// Egyetlen DOM művelet => egyetlen reflow => gyors!
✔ Teljesítmény tipp
A DOM manipuláció „drága" művelet, mert a böngészőnek újra kell számolnia az elrendezést (reflow) és újra kell rajzolnia a képernyőt (repaint). Minimalizáljuk a DOM módosítások számát: használjunk DocumentFragment-et, vagy építsük fel az egész HTML-t egy változóban és szúrjuk be egyszer.

2.4 Eseménykezelés

Hogyan működnek az események?

Az események a felhasználó és a böngésző interakcióit jelzik: kattintás, billentyűleütés, űrlap beküldés, scroll stb. A JavaScript-ben listener (figyelő) függvényeket regisztrálunk, amelyek az esemény bekövetkeztekor automatikusan lefutnak. Ez az aszinkron, eseményvezérelt programozás alapja.

addEventListener és removeEventListener

Az addEventListener() metódussal regisztrálunk egy listenert egy adott eseménytípusra. Fontos: ha később el akarjuk távolítani a listenert, nevesített függvényt kell használnunk (nem anonim függvényt), mert a removeEventListener()-nek ugyanazt a függvény-referenciát kell kapnia.

var btn = document.querySelector("#my-btn");

// Nevesített függvény => eltávolítható!
function handleClick(event) {
  console.log("Kattintás történt!");
  console.log("Cél elem:", event.target);
}

// Regisztráció
btn.addEventListener("click", handleClick);

// Eltávolítás (ugyanaz a függvény-referencia kell!)
btn.removeEventListener("click", handleClick);

A harmadik paraméter: options

Az addEventListener harmadik paramétereként opciókat adhatunk meg:

btn.addEventListener("click", handleClick, {
  once: true,     // Egyszer fut, aztán automatikusan eltávolítódik.
                  // Hasznos: "Megrendelés" gomb – ne lehessen duplán kattintani.

  capture: false, // Melyik fázisban kapjuk el? false = bubbling (alapért.)
                  // true = capturing fázis (kívülről befelé)

  passive: true,  // Jelezzük, hogy NEM fogjuk meghívni a preventDefault()-ot.
                  // A böngésző optimalizálhatja a scrolling-ot.
                  // Scroll és touch eseményeknél ajánlott.
});

Az Event objektum

Amikor egy esemény bekövetkezik, a böngésző létrehoz egy Event objektumot, amely részletes információt tartalmaz az eseményről. Ezt a listener függvény első paramétereként kapja meg:

Tulajdonság / MetódusLeírásHasználat
event.targetAz elem, amelyen az esemény ténylegesen történtDelegálásnál a belső elem azonosítása
event.currentTargetAz elem, amelyre a listener regisztrálva vanMindig a „saját" elemünk
event.typeAz esemény típusa (string)"click", "submit", "keydown" stb.
event.preventDefault()Alapértelmezett viselkedés tiltásaForm submit, link navigáció megakadályozása
event.stopPropagation()Esemény terjedésének leállításaRitkán szükséges – óvatosan!
// preventDefault() – űrlap beküldés megakadályozása
var form = document.querySelector("#my-form");

form.addEventListener("submit", function(event) {
  event.preventDefault();  // Az oldal NEM töltődik újra!

  var formData = new FormData(form);
  var data = Object.fromEntries(formData);
  console.log("Űrlap adatok:", data);

  // Küldés Fetch-csel az oldal újratöltése helyett
  fetch("/api/submit", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
});

Esemény terjedés: bubbling és capturing

Az események a DOM fában terjednek. Ez három fázisban történik:

  1. Capturing fázis – az esemény a gyökértől (document) lefelé halad a célelemig
  2. Target fázis – az esemény eléri a tényleges célelemet
  3. Bubbling fázis – az esemény a célelemtől felfelé „buborékol" a gyökérig

Alapértelmezetten a bubbling fázisban kapjuk el az eseményeket. Ez azt jelenti, hogy ha egy gombra kattintunk, az eseményt a gomb szülője, nagyszülője stb. is „meghallja".

// HTML: <div class="outer"><button class="inner">Katt</button></div>

document.querySelector(".outer").addEventListener("click", function() {
  console.log("outer kattintás");
});

document.querySelector(".inner").addEventListener("click", function() {
  console.log("inner kattintás");
});

// A .inner gombra kattintva a kimenet (bubbling sorrend):
// "inner kattintás"  (target fázis)
// "outer kattintás"  (bubbling: felfelé terjedt a szülőhöz)

Eseménydelegálás (Event Delegation)

A bubbling-ot kihasználva egyetlen listenert helyezhetünk a szülő elemre, amely az összes gyermekelem eseményét elkapja. Ez két nagy előnnyel jár:

✔ Delegálás
// Egyetlen listener a listán, amely kezeli az összes törlés gombot
var todoList = document.querySelector(".todo-list");

todoList.addEventListener("click", function(event) {
  // A closest() megkeresi a legközelebbi őst (vagy önmagát),
  // amely illeszkedik a szelektorra. Ha nem talál: null.
  var deleteBtn = event.target.closest("[data-action='delete']");
  if (!deleteBtn) return;  // Nem törlés gombra kattintottak

  // A gombtól felfelé keressük a listaelemet
  var item = deleteBtn.closest(".todo-item");
  if (!item) return;

  var id = item.dataset.id;
  item.remove();
  console.log("Törölve: " + id);
});

// Ez működik DINAMIKUSAN hozzáadott elemekre is!
// Ha programozottan hozzáadunk egy új .todo-item-et,
// a törlés gombja automatikusan működik, mert a szülő figyel.
ℹ Miért closest() és nem target?
Az event.target a LEGBELSŐ elem, amelyre kattintottunk. Ha a gomb belsejében egy <span> ikon van, a target az ikon lesz, nem a gomb! A closest() felfelé keresi a megadott szelektort, így biztosan megtalálja a gombot (vagy annak szülőjét).

Debounce és throttle

Egyes események nagyon sűrűn tüzelnek – az input esemény minden billentyűleütésnél, a scroll akár másodpercenként 60-szor. Ha ezekre drága műveletet (pl. API hívás, DOM átrendezés) kötünk, az alkalmazás belassulhat. A megoldás a hívások korlátozása:

MintaMűködésAnalógiaHasználat
debounceCsak a LEGUTOLSÓ hívás után X ms-sel futLift: megvárja, amíg mindenki beszálltKeresőmező, űrlap validáció
throttleMaximum X ms-enként egyszer futCsap: egyenletesen csöpögScroll, resize, egérmozgatás
// Debounce implementáció
// A függvény csak akkor fut le, ha delayMs ideje nem hívták meg újra.
// Minden egyes hívás "újraindítja az órát".
function debounce(fn, delayMs) {
  var timerId;
  return function() {
    var context = this;
    var args = arguments;
    clearTimeout(timerId);          // Töröljük az előző időzítőt
    timerId = setTimeout(function() {
      fn.apply(context, args);        // delayMs elteltével lefut
    }, delayMs);
  };
}

// Használat: keresőmező – 300ms-ot várunk a gépelés befejezése után
var searchInput = document.querySelector("#search");
searchInput.addEventListener("input", debounce(function(event) {
  console.log("Keresés indítása: " + event.target.value);
  // Itt hívhatnánk: api.get("/search?q=" + event.target.value)
}, 300));
// Throttle implementáció
// A függvény legfeljebb limitMs-enként egyszer fut le.
function throttle(fn, limitMs) {
  var waiting = false;
  return function() {
    if (waiting) return;             // Még nem telt le az idő
    fn.apply(this, arguments);
    waiting = true;
    setTimeout(function() {
      waiting = false;              // limitMs után újra engedélyez
    }, limitMs);
  };
}

// Használat: scroll esemény – legfeljebb 100ms-enként
window.addEventListener("scroll", throttle(function() {
  console.log("Scroll pozíció: " + window.scrollY);
}, 100));

2.5 Web Storage API

Mi az a Web Storage?

A Web Storage API lehetővé teszi, hogy a böngészőben kulcs-érték párokat tároljunk. Kétféle tároló érhető el, amelyek hasonlóan működnek, de eltérő élettartammal rendelkeznek:

SzempontlocalStoragesessionStorage
ÉlettartamTartós – böngésző bezárás után is megmaradMunkamenet végéig – a fül bezárásakor törlődik
MegosztásUgyanazon origin minden füle/ablaka látjaFülenként/ablakonként külön
Méretkorlát~5 MB (origin-enként)~5 MB (origin-enként)
Tipikus használatTéma, nyelv, kosár, beállításokWizard lépései, ideiglenes szűrők

Mindkét tároló szinkron – azonnal visszaadja az eredményt (nem kell await). Fontos korlátozás: csak stringeket tárolhatnak. Ha objektumot vagy tömböt akarunk menteni, JSON.stringify()-jal alakítjuk szöveggé, és visszaolvasáskor JSON.parse()-szal.

Alap műveletek

// ÍRÁS – kulcs-érték pár mentése
localStorage.setItem("theme", "dark");
localStorage.setItem("lang", "hu");

// OLVASÁS – ha nem létezik a kulcs, null-t kapunk
var theme = localStorage.getItem("theme");  // "dark"
var foo = localStorage.getItem("foo");      // null (nem létezik)

// TÖRLÉS – egy kulcs eltávolítása
localStorage.removeItem("theme");

// AZ ÖSSZES TÖRLÉSE – figyelem, ez mindent kitöröl!
localStorage.clear();

// Kulcsok száma
console.log(localStorage.length);  // pl. 2

Objektumok tárolása: JSON

A Storage csak stringeket kezel. Ha objektumot próbálunk közvetlenül elmenteni, a böngésző a .toString() metódust hívja meg rajta, aminek eredménye "[object Object]" – ami nyilvánvalóan nem az, amit szeretnénk:

✘ Hibás – adatvesztés!
// Objektum közvetlenül => "[object Object]"!
localStorage.setItem("user", { name: "János", age: 25 });
var result = localStorage.getItem("user");
console.log(result);  // "[object Object]" – az adatok ELVESZTEK!
✔ Helyes – JSON
// Mentés: objektum => JSON string
var user = { name: "János", age: 25, hobbies: ["futás", "olvasás"] };
localStorage.setItem("user", JSON.stringify(user));

// Visszaolvasás: JSON string => objektum
var stored = JSON.parse(localStorage.getItem("user"));
console.log(stored.name);      // "János"
console.log(stored.hobbies[0]); // "futás"

Biztonságos storage wrapper

Érdemes egy wrapper függvényeket készíteni, amelyek kezelik a JSON konverziót, a null ellenőrzést és a tárhelyhiány (QuotaExceededError) hibát:

var storage = {
  // Értékmentés – JSON-ként tárolja
  set: function(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("Storage írási hiba:", e.message);
      // QuotaExceededError: a tárhely megtelt (~5 MB)
    }
  },

  // Értékolvasás – ha nem létezik, a defaultValue-t adja
  get: function(key, defaultValue) {
    try {
      var item = localStorage.getItem(key);
      return item !== null ? JSON.parse(item) : defaultValue;
    } catch (e) {
      // JSON.parse hiba (sérült adat)
      return defaultValue;
    }
  },

  remove: function(key) {
    localStorage.removeItem(key);
  },
};

// Használat:
storage.set("settings", { theme: "dark", fontSize: 16 });
var settings = storage.get("settings", { theme: "light", fontSize: 14 });
console.log(settings.theme);  // "dark"
⚠ Biztonsági figyelmeztetés
SOHA ne tároljunk érzékeny adatot (jelszó, JWT token, személyes azonosító) a localStorage-ban vagy a sessionStorage-ban! Egy XSS támadás (lásd 3.1 fejezet) teljes hozzáférést kap a Storage tartalmához. A JWT-t HttpOnly cookie-ban tároljuk (lásd 3.4 fejezet).

2.6 Timer és animációs API-k

setTimeout – egyszeri késleltetés

A setTimeout() egy adott idő elteltével egyszer futtatja le a megadott függvényt. Fontos: a megadott idő minimális várakozás, nem garancia – ha a főszál foglalt, a callback csak később fut le (lásd 1.1 Event Loop).

// 2 mp (2000 ms) múlva fut le – EGYSZER
var timerId = setTimeout(function() {
  console.log("Ez 2 másodperc múlva jelenik meg.");
}, 2000);

// Törlés – ha a timeout még nem futott le, megakadályozhatjuk
clearTimeout(timerId);

// Gyakorlati példa: értesítés automatikus eltűntetése
function showNotification(message, durationMs) {
  var el = document.querySelector(".notification");
  el.textContent = message;
  el.classList.add("visible");

  setTimeout(function() {
    el.classList.remove("visible");
  }, durationMs);
}

showNotification("Mentés sikeres!", 3000);

setInterval – ismétlődő végrehajtás

A setInterval() a megadott időközönként ismételten futtatja a callback-et. Nagyon fontos az interval ID mentése és a megfelelő leállítás, különben memóriaszivárgást okozhatunk!

// Visszaszámláló
var remaining = 10;
var display = document.querySelector("#countdown");

var intervalId = setInterval(function() {
  remaining--;
  display.textContent = remaining + " mp";

  if (remaining <= 0) {
    clearInterval(intervalId);  // FONTOS: leállítjuk!
    display.textContent = "Lejárt!";
  }
}, 1000);

setInterval veszélye aszinkron feladatoknál

Ha a callback aszinkron (pl. fetch hívás), és a végrehajtási idő hosszabb, mint az intervallum, a hívások „halmozódnak" – az előző még fut, amikor az újabb indul. A megoldás: rekurzív setTimeout.

✘ Halmozódik!
// Ha a fetch 3 mp-ig tart, de az intervallum 1 mp
// => 3 kérés indul, mielőtt az első befejeződik!
setInterval(async function() {
  var data = await fetch("/api/status").then(function(r) { return r.json(); });
  updateDashboard(data);
}, 1000);
✔ Rekurzív setTimeout
// Csak a BEFEJEZÉS UTÁN ütemez újat => nincs halmozódás
async function pollStatus() {
  try {
    var data = await fetch("/api/status").then(function(r) { return r.json(); });
    updateDashboard(data);
  } catch (err) {
    console.error("Polling hiba:", err);
  }
  setTimeout(pollStatus, 1000);  // Csak most ütemezzük az újat
}
pollStatus();  // Indítás

requestAnimationFrame – animációk

A requestAnimationFrame() (rAF) a böngésző renderelési ciklusához igazodik – a callback közvetlenül a következő képkocka rajzolása ELŐTT fut le. Ez tökéletes szinkronizációt biztosít az animációkhoz:

setInterval(fn, 16)requestAnimationFrame(fn)
Pontosság~16ms, de csúszhatPontosan a képkocka előtt
Háttér fülTovábbra is fut (CPU!)Automatikusan szünetel
FPSFix (pl. 60)Alkalmazkodik (60/120/144 Hz)
BatchingNem optimalizáltA böngésző összevonja a DOM olvasásokkal
// Időalapú animáció – azonos sebesség MINDEN monitoron
// A delta time segítségével nem számít, hogy 60 vagy 144 Hz a kijelző
var box = document.querySelector(".animated-box");
var position = 0;
var lastTime = null;
var SPEED = 200;  // 200 pixel / másodperc

function animate(timestamp) {
  // Az első híváskor nincs még lastTime
  if (lastTime === null) lastTime = timestamp;

  // Delta time: az előző képkocka óta eltelt idő (mp-ben)
  var deltaTime = (timestamp - lastTime) / 1000;
  lastTime = timestamp;

  // Pozíció frissítése: sebesség * idő = út
  position += SPEED * deltaTime;
  box.style.transform = "translateX(" + position + "px)";

  // Amíg nem ért 500px-re, folytatjuk
  if (position < 500) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);  // Indítás
✔ Tipp
Ha CSS animációval (transition, @keyframes) megoldható a feladat, MINDIG azt preferáljuk – a böngésző GPU-n futtatja, és sokkal hatékonyabb. A requestAnimationFrame-et komplex, programozott animációkhoz használjuk, ahol a CSS nem elég (pl. canvas rajzolás, fizika szimulációk, játékok).

2.7 Egyéb hasznos böngésző API-k

Geolocation API

A Geolocation API lehetővé teszi a felhasználó földrajzi pozíciójának lekérdezését. A böngésző engedélyt kér a felhasználótól, és csak HTTPS-en működik (biztonsági okokból).

// Egyszeri pozíció lekérdezés
navigator.geolocation.getCurrentPosition(
  // Sikeres callback
  function(pos) {
    console.log("Szélesség: " + pos.coords.latitude);
    console.log("Hosszúság: " + pos.coords.longitude);
    console.log("Pontosság: " + pos.coords.accuracy + " m");
  },
  // Hiba callback
  function(err) {
    if (err.code === 1) console.log("Felhasználó megtagadta az engedélyt");
    if (err.code === 2) console.log("Pozíció nem elérhető");
    if (err.code === 3) console.log("Időtúllépés");
  },
  // Opciók
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
);

// Folyamatos követés (pl. navigáció)
var watchId = navigator.geolocation.watchPosition(function(pos) {
  updateMap(pos.coords.latitude, pos.coords.longitude);
});

// Leállítás
navigator.geolocation.clearWatch(watchId);

Clipboard API

A Clipboard API aszinkron hozzáférést biztosít a rendszer vágólapjához. Engedélyt igényelhet a böngészőtől.

// Szöveg másolása a vágólapra
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    showToast("Vágólapra másolva!");
  } catch (err) {
    console.error("Másolás sikertelen:", err);
  }
}

// "Másolás" gomb implementálása
document.querySelector("#copy-btn").addEventListener("click", function() {
  var codeBlock = document.querySelector("pre code");
  copyToClipboard(codeBlock.textContent);
});

IntersectionObserver – elemek láthatóságának figyelése

Az IntersectionObserver hatékonyan figyeli, hogy egy elem mikor kerül a viewport-ba (láthatóvá válik). Két klasszikus felhasználása: lazy loading (képek késleltetett betöltése) és infinite scroll (végtelen görgetés).

// Lazy loading: a képeket csak akkor töltjük be, amikor közel kerülnek
// HTML: <img class="lazy" data-src="/images/photo.jpg" alt="Fotó">

var lazyObserver = new IntersectionObserver(
  // Callback: minden figyelt elemre meghívódik, ha változik a láthatóság
  function(entries) {
    entries.forEach(function(entry) {
      if (entry.isIntersecting) {
        var img = entry.target;
        img.src = img.dataset.src;         // data-src => src (betöltés indul)
        img.classList.remove("lazy");      // Opcionális: stílus váltás
        lazyObserver.unobserve(img);       // Leállítjuk a figyelést
      }
    });
  },
  // Opciók: 200px-szel ELŐBB betöltjük (a viewport szélétől)
  { rootMargin: "200px" }
);

// Minden lazy képet figyelni kezdünk
document.querySelectorAll("img.lazy").forEach(function(img) {
  lazyObserver.observe(img);
});
ℹ Egyszerű esetekhez
Ha csak egyszerű képek lazy loading-ját szeretnénk, a HTML <img loading="lazy"> attribútum is elég – nem kell JavaScript. Az IntersectionObserver-t összetettebb logikához (animált belépés, infinite scroll, analytics tracking) használjuk.

FormData – űrlapadatok kezelése

A FormData automatikusan kiolvassa az űrlap mezőit, és kényelmesen küldhető Fetch-csel. Két fő küldési mód:

var form = document.querySelector("#registration-form");

form.addEventListener("submit", async function(event) {
  event.preventDefault();

  var formData = new FormData(form);  // Automatikusan kiolvassa a mezőket

  // 1. mód: JSON küldés (ha nincs fájl)
  var jsonData = Object.fromEntries(formData);
  await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(jsonData),
  });

  // 2. mód: FormData küldés (fájlfeltöltésnél)
  // FONTOS: NE állítsuk be a Content-Type fejlécet!
  // A böngésző automatikusan beállítja a multipart/form-data-t
  // a megfelelő boundary-vel.
  await fetch("/api/upload", {
    method: "POST",
    body: formData,  // Közvetlenül a FormData objektumot adjuk
  });
});

History API és URL API

A History API lehetővé teszi az URL módosítását az oldal újratöltése nélkül – ez az SPA (Single Page Application) routing alapja. Az URL API pedig az URL-ek strukturált elemzésére és módosítására szolgál.

// URL módosítása újratöltés nélkül
history.pushState(
  { page: "products", id: 42 },  // state objektum (popstate-ben kapjuk)
  "",                             // title (a legtöbb böngésző ignorálja)
  "/products/42"                  // az új URL (megjelenik a címsorban)
);

// Vissza/Előre gomb kezelése
window.addEventListener("popstate", function(event) {
  if (event.state) {
    console.log("Navigáció: " + event.state.page);
    renderPage(event.state);
  }
});

// URL elemzése az URL API-val
var url = new URL("https://example.com/search?q=javascript&page=2#results");
console.log(url.pathname);               // "/search"
console.log(url.searchParams.get("q"));   // "javascript"
console.log(url.searchParams.get("page"));// "2"
console.log(url.hash);                    // "#results"

// URL építése
url.searchParams.set("page", "3");
console.log(url.toString());
// "https://example.com/search?q=javascript&page=3#results"
ℹ A 2. fejezet összefoglalása
2.1 Fetch – Promise alapú HTTP; response.ok KÖTELEZŐ; AbortController timeout; API wrapper.
2.2 REST – erőforrás URL-ek; CRUD metódusok; státuszkódok; JSON válaszformátumok.
2.3 DOM – querySelector/All; textContent (biztonságos!); classList; createElement; DocumentFragment.
2.4 Események – addEventListener; bubbling/capturing; delegálás (closest); debounce/throttle.
2.5 Storage – localStorage (tartós) vs. sessionStorage; JSON.stringify/parse; biztonságos wrapper.
2.6 Timer – setTimeout/setInterval; rekurzív setTimeout aszinkron munkához; requestAnimationFrame delta time.
2.7 Egyéb – Geolocation, Clipboard, IntersectionObserver (lazy loading), FormData, History/URL API.