Vollständiges JavaScript & TypeScript Curriculum (8 Module über 3 Lehrpfade: ES6 Grundlagen, Asynchrones JS & Fetch, TypeScript & Web-App Abschlussprojekt)

This commit is contained in:
Alex Mueller 2026-08-30 00:42:16 +02:00
parent aff02ca52e
commit 1916a634f4
38 changed files with 3799 additions and 14 deletions

View file

@ -13,5 +13,5 @@
"streak_tage": 1,
"letzter_aktiver_tag": "2026-08-29",
"historie": [],
"zuletzt_aktualisiert": "2026-08-30T00:34:34.221863"
"zuletzt_aktualisiert": "2026-08-30T00:42:10.922005"
}

View file

@ -37,9 +37,25 @@
titel = "Python Professional & Data Engineering";
moduleDesc = "6 Master-Module: Reguläre Ausdrücke (re), Functional Programming (lambda, zip, map), Generatoren & itertools, Dataclasses & Type Hints, Relationale DBs mit SQLite, REST-APIs & JSON-Feeds";
trackKey = "lehrpfad_4";
} else if (lehrpfadNummer === "js_grundlagen") {
titel = "JavaScript ES6+ Grundlagen & DOM-Manipulation";
moduleDesc = "4 Module: Variablen (let/const), Arrow Functions, Arrays (map/filter/reduce), Objekte, Destructuring und DOM-Event-Driven Architecture";
trackKey = "js_grundlagen";
} else if (lehrpfadNummer === "js_async") {
titel = "Asynchrones JavaScript & REST-APIs";
moduleDesc = "2 Module: Callbacks, Event Loop, Promises, Chaining, Promise.all, Async/Await und REST-APIs mit Fetch & JSON";
trackKey = "js_async";
} else if (lehrpfadNummer === "js_master") {
titel = "Master of Modern JavaScript & TypeScript Development";
moduleDesc = "Vollständiger Abschluss des JS/TS Curriculums (8 Module, TypeScript Type Safety, Interfaces, Generics & Task-App Masterprojekt)";
trackKey = "js_master";
} else if (lehrpfadNummer === "sql_grundlagen") {
titel = "SQL & Relationale Datenbanken";
moduleDesc = "CREATE TABLE, Primary Keys, INSERT, SELECT, Filter mit WHERE und SQLite WebAssembly Abfragen";
trackKey = "sql_grundlagen";
} else if (lehrpfadNummer === "master") {
titel = "Grandmaster of Python Software Engineering";
moduleDesc = "Vollständiger Abschluss aller 4 Lehrpfade (33 Module, über 220 bestandene automatisierte Unittests, SQLite-Persistenz, REST-APIs & MVC-Desktop-App)";
titel = "Grandmaster of Software Engineering (Polyglot)";
moduleDesc = "Vollständiger Abschluss der plattformweiten Curricula (Python, JavaScript, TypeScript & SQL)";
trackKey = "master";
}

View file

@ -14,7 +14,7 @@ window.GAMIFICATION_DATA = {
"streak_tage": 1,
"letzter_aktiver_tag": "2026-08-29",
"historie": [],
"zuletzt_aktualisiert": "2026-08-30T00:34:34.221863"
"zuletzt_aktualisiert": "2026-08-30T00:42:10.922005"
};
window.GAMIFICATION_BADGES_KATALOG = {
"rechenkonegin": {

View file

@ -72,8 +72,19 @@
try {
const runFn = new Function(e.data.code);
runFn();
self.postMessage({ type: 'done', success: true });
const result = runFn();
if (result && typeof result.then === 'function') {
result
.then(() => {
self.postMessage({ type: 'done', success: true });
})
.catch((err) => {
self.postMessage({ type: 'stderr', text: '\\n❌ Laufzeitfehler: ' + (err && err.stack ? err.stack : (err && err.message ? err.message : String(err))) + '\\n' });
self.postMessage({ type: 'done', success: false, error: err ? (err.message || String(err)) : 'Fehler' });
});
} else {
self.postMessage({ type: 'done', success: true });
}
} catch (err) {
self.postMessage({ type: 'stderr', text: '\\n❌ Laufzeitfehler: ' + err.stack + '\\n' });
self.postMessage({ type: 'done', success: false, error: err.message });
@ -152,6 +163,32 @@
throw new Error(msg || 'Bedingung ist nicht wahr');
}
passed++;
},
async rejects(promiseOrFn, expectedMsg, msg) {
total++;
let threw = false;
let caughtError = null;
try {
if (typeof promiseOrFn === 'function') {
await promiseOrFn();
} else {
await promiseOrFn;
}
} catch (err) {
threw = true;
caughtError = err;
}
if (!threw) {
failures++;
throw new Error(msg || 'Promise sollte abgelehnt werden (reject), wurde aber erfolgreich aufgelöst.');
}
if (expectedMsg && typeof expectedMsg === 'string') {
if (!String(caughtError && (caughtError.message || caughtError)).includes(expectedMsg)) {
failures++;
throw new Error(msg || ('Erwartete Fehlermeldung "' + expectedMsg + '" nicht gefunden in: ' + caughtError));
}
}
passed++;
}
};
@ -159,9 +196,9 @@
${userCode}
// 2. Test-Suite ausführen
${testCode}
({ total, passed, failures });
return (async () => {
${testCode}
})();
`;
this.runCode(fullScript, logCallback).then((res) => {

View file

@ -45,8 +45,9 @@ console.log(text);</code></pre>
<label class="task-label" for="t3"><strong>TODO 3:</strong> Funktion <code>istVolljaehrig(alter)</code> schreiben.</label>
</div>
<div style="margin-top: 20px;">
<div style="margin-top: 20px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=01_erste_schritte_variablen" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen &rarr;</a>
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">Weiter zu JS 02 &rarr;</a>
</div>
</section>
</main>

View file

@ -42,8 +42,10 @@ const summe = zahlen.reduce((a, b) => a + b, 0);</code></pre>
<label class="task-label" for="t3"><strong>TODO 3:</strong> Funktion <code>arraySumme(zahlen)</code> mit <code>.reduce()</code> schreiben.</label>
</div>
<div style="margin-top: 20px;">
<div style="margin-top: 20px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=02_funktionen_und_arrays" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen &rarr;</a>
<a href="../01_erste_schritte_variablen/index.html" class="btn btn-secondary">&larr; Zurück zu JS 01</a>
<a href="../03_objekte_und_destructuring/index.html" class="btn btn-secondary">Weiter zu JS 03 &rarr;</a>
</div>
</section>
</main>

View file

@ -0,0 +1,118 @@
# JS 03: Objekte, Destructuring & Rest/Spread 📦
Willkommen zu **Modul 03** des JavaScript-Kurses!
Objekte sind der Herzschlag von JavaScript: Fast alles in JavaScript ist ein Objekt oder verhält sich wie eines. In diesem Modul lernst du, wie du strukturierte Daten erstellst, mit modernem **ES6-Destructuring** blitzschnell entpackst und Daten mit dem **Spread- & Rest-Operator (`...`)** elegant transformierst.
---
## 💡 1. Das Wichtigste in Kürze
### A. Object Literals & Property Shorthand
Ein Objekt speichert Daten als **Schlüssel-Wert-Paare** (*Key-Value Pairs*):
```javascript
// Klassische Deklaration
const person = {
name: "Max",
alter: 25,
istEntwickler: true
};
// ES6 Property Shorthand (Wenn Variablenname und Key identisch sind)
const name = "Anna";
const email = "anna@example.com";
const user = { name, email };
// Entspricht: { name: name, email: email }
```
### B. Destructuring Assignment (Objekte & Arrays entpacken)
Mit Destructuring kannst du Werte direkt aus Objekten oder Arrays extrahieren, ohne wiederholt `objekt.eigenschaft` schreiben zu müssen:
```javascript
const dev = { name: "Sarah", sprache: "JavaScript", erfahrung: 5 };
// 🎯 Object Destructuring:
const { name, sprache } = dev;
console.log(name); // "Sarah"
console.log(sprache); // "JavaScript"
// 🎯 Alias (Umbennen) & Standardwerte (Default Values):
const { alter = 18, name: vorname } = dev;
console.log(alter); // 18 (Default-Wert, da 'alter' im Objekt fehlt)
console.log(vorname); // "Sarah" (unter neuem Namen verfügbar)
// 🎯 Array Destructuring:
const farben = ["#ff0000", "#00ff00", "#0000ff"];
const [rot, gruen] = farben;
console.log(rot); // "#ff0000"
```
### C. Spread- & Rest-Operator (`...`)
Die drei Punkte `...` haben je nach Kontext zwei mächtige Funktionen:
1. **Spread-Operator (Ausbreiten / Zusammenfügen):**
Erzeugt eine flache Kopie (*Shallow Copy*) oder kombiniert mehrere Objekte/Arrays ohne Mutation:
```javascript
const basisInfo = { name: "Tom", alter: 30 };
const kontakt = { email: "tom@web.de", stadt: "Berlin" };
// Neues Objekt mit allen Eigenschaften beider Objekte:
const vollstaendig = { ...basisInfo, ...kontakt, premium: true };
```
2. **Rest-Operator (Einsammeln des Rests):**
Sammelt verbleibende Eigenschaften in einem neuen Objekt/Array:
```javascript
const { email, ...andereDaten } = vollstaendig;
console.log(email); // "tom@web.de"
console.log(andereDaten); // { name: "Tom", alter: 30, stadt: "Berlin", premium: true }
```
### D. Hilfsmethoden: `Object.keys()`, `Object.values()`, `Object.entries()`
```javascript
const auto = { marke: "Tesla", modell: "Model 3", baujahr: 2024 };
console.log(Object.keys(auto)); // ["marke", "modell", "baujahr"]
console.log(Object.values(auto)); // ["Tesla", "Model 3", 2024]
console.log(Object.entries(auto)); // [["marke", "Tesla"], ["modell", "Model 3"], ["baujahr", 2024]]
console.log(Object.keys(auto).length); // 3 (Anzahl der Eigenschaften)
```
---
## 🧰 Die didaktische Analogie: "Die Werkzeugkiste & Auspack-Station"
- **Das Objekt als Werkzeugkiste:**
Ein Objekt `{ hammer: "500g", saege: "Holz", bohrer: "Akku" }` ist wie eine strukturierte Werkzeugkiste mit beschrifteten Fächern.
- **Destructuring als gezieltes Herausgreifen:**
Statt die ganze schwere Kiste an die Baustelle zu tragen (`werkzeugkiste.hammer`), greifst du mit `const { hammer, saege } = werkzeugkiste;` genau die Werkzeuge heraus, die du für die aktuelle Arbeit brauchst.
- **Spread (`...`) als Zusammenlegen in eine neue Kiste:**
Mit `{ ...kiste1, ...kiste2, multimeter: "digital" }` schüttest du zwei Werkzeugkisten in eine neue, größere Kiste um und packst noch ein neues Werkzeug dazu – die ursprünglichen Kisten bleiben unberührt (*Immutability*)!
---
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
Öffne die Datei `aufgabe.js` und löse die 5 Teilziele:
1. **TODO 1:** `erstelleBenutzer(name, email, rolle = "user")`
Erstelle und gib ein Objekt mit `{ name, email, rolle }` unter Verwendung des Property Shorthands zurück.
2. **TODO 2:** `extrahiereDetails(benutzerObjekt)`
Extrahiere `name` und `email` mit Destructuring und gib den String `"${name} (${email})"` zurück.
3. **TODO 3:** `fuegeAdresseHinzu(benutzerObjekt, adresseObjekt)`
Erzeuge mit Spread (`...`) ein neues, zusammengeführtes Objekt `{ ...benutzerObjekt, ...adresseObjekt }`, ohne `benutzerObjekt` zu verändern.
4. **TODO 4:** `zaehleEigenschaften(objekt)`
Ermittle die Anzahl der Eigenschaften eines Objekts mit `Object.keys(objekt).length`.
5. **TODO 5:** `berechneWarenkorbGesamtwert(warenkorbItems)`
Berechne die Summe aus `preis * anzahl` für alle Items im Array. Nutze Destructuring (`const { preis, anzahl = 1 }`) im Schleifendurchlauf oder in `.reduce()`.
---
## 🧪 Tests ausführen
Führe den Test in der interaktiven Web-IDE oder im Runner aus. Deine Lösung ist perfekt, wenn alle Tests erfolgreich durchlaufen!

View file

@ -0,0 +1,46 @@
/**
* 🌐 JS 03: OBJEKTE, DESTRUCTURING & REST/SPREAD 🌐
* ==================================================
* Strukturierte Daten & moderne ES6-Operatoren in JavaScript.
*/
// 🎯 TEILZIEL 1 (TODO 1): erstelleBenutzer(name, email, rolle = "user")
// Erstelle ein Benutzerobjekt mit Property Shorthand und Default-Parameter.
// Rückgabe: Ein Objekt mit { name, email, rolle }
function erstelleBenutzer(name, email, rolle = "user") {
// TODO: Nutze ES6 Property Shorthand { name, email, rolle }
return null;
}
// 🎯 TEILZIEL 2 (TODO 2): extrahiereDetails(benutzerObjekt)
// Extrahiere 'name' und 'email' mittels Object Destructuring: const { name, email } = benutzerObjekt;
// Rückgabe: Formatierter String im Format: `${name} (${email})`
function extrahiereDetails(benutzerObjekt) {
// TODO: Destructuring anwenden und String im Format "Name (email@domain.de)" zurückgeben
return "";
}
// 🎯 TEILZIEL 3 (TODO 3): fuegeAdresseHinzu(benutzerObjekt, adresseObjekt)
// Kombiniere Benutzer und Adresse zu einem NEUEN Objekt mittels Spread-Operator: { ...benutzerObjekt, ...adresseObjekt }
// Wichtig: Das Originalobjekt benutzerObjekt darf NICHT verändert werden (Immutability).
function fuegeAdresseHinzu(benutzerObjekt, adresseObjekt) {
// TODO: Neues Objekt mit Object Spread { ...benutzerObjekt, ...adresseObjekt } erstellen
return null;
}
// 🎯 TEILZIEL 4 (TODO 4): zaehleEigenschaften(objekt)
// Zähle die Anzahl der eigenen Schlüssel/Eigenschaften eines Objekts mit Object.keys(objekt).length.
// Bei leeren oder ungültigen Objekten soll 0 zurückgegeben werden.
function zaehleEigenschaften(objekt) {
// TODO: Object.keys() nutzen und Anzahl der Keys zurückgeben
return 0;
}
// 🎯 TEILZIEL 5 (TODO 5): berechneWarenkorbGesamtwert(warenkorbItems)
// Berechne die Summe aller (preis * anzahl) im Array warenkorbItems.
// Nutze Destructuring im Loop oder in reduce: for (const { preis, anzahl = 1 } of warenkorbItems)
// Bei leerem Warenkorb [] soll 0 zurückgegeben werden.
function berechneWarenkorbGesamtwert(warenkorbItems) {
// TODO: Destructuring im Schleifendurchlauf oder reduce nutzen
return 0;
}

View file

@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS 03: Objekte, Destructuring &amp; Rest/Spread</title>
<link rel="stylesheet" href="../../../assets/style.css">
</head>
<body>
<div class="app-layout">
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🌐 JavaScript Grundlagen</div>
<h1>JS 03: Objekte, Destructuring &amp; Rest/Spread</h1>
<p>Lerne strukturierte Datenmodellierung mit Object Literals, ES6-Destructuring und dem Rest/Spread-Operator.</p>
</div>
</header>
<main class="container">
<!-- 1. THEORIE: OBJEKTE & SHORTHAND -->
<section class="card">
<h2>💡 1. Object Literals &amp; Property Shorthand</h2>
<p>Objekte speichern zusammengehörige Informationen als Schlüssel-Wert-Paare. Modernes JavaScript (ES6) bietet mit dem <strong>Property Shorthand</strong> eine extrem prägnante Syntax, wenn Variablen- und Eigenschaftsname übereinstimmen:</p>
<pre><code>const name = "Max Mustermann";
const email = "max@beispiel.de";
// Vor ES6 (umständlich):
const alterUser = { name: name, email: email };
// Mit modernem ES6 Property Shorthand (sauber & prägnant):
const benutzer = { name, email, rolle: "user" };
console.log(benutzer); // { name: "Max Mustermann", email: "max@beispiel.de", rolle: "user" }</code></pre>
</section>
<!-- 2. THEORIE: DESTRUCTURING -->
<section class="card">
<h2>📦 2. Destructuring Assignment (Auspacken von Daten)</h2>
<p>Mit <strong>Destructuring</strong> entpackst du Werte direkt in separate Variablen. Das spart Code und erhöht die Lesbarkeit spürbar:</p>
<pre><code>const person = { vorname: "Anna", stadt: "Berlin", punkte: 95 };
// 🎯 Object Destructuring:
const { vorname, stadt } = person;
console.log(vorname); // "Anna"
// 🎯 Destructuring mit Standardwerten und Aliases (Umbenennung):
const { land = "Deutschland", punkte: score } = person;
console.log(land); // "Deutschland" (Default, da 'land' in person nicht existiert)
console.log(score); // 95 (Alias für person.punkte)
// 🎯 Array Destructuring:
const koordinaten = [52.52, 13.405];
const [breite, laenge] = koordinaten;
console.log(breite, laenge); // 52.52, 13.405</code></pre>
</section>
<!-- 3. THEORIE: SPREAD & REST -->
<section class="card">
<h2>✨ 3. Spread- &amp; Rest-Operator (<code>...</code>)</h2>
<p>Die drei Punkte (<code>...</code>) ermöglichen das Ausbreiten (Spread) und Einsammeln (Rest) von Datenstrukturen:</p>
<pre><code>const basisProfil = { name: "Lisa", alter: 28 };
const kontaktInfo = { email: "lisa@dev.io", stadt: "Hamburg" };
// 🌟 Spread: Objekte verschmelzen ohne Mutation (Immutability):
const vollstaendigesProfil = { ...basisProfil, ...kontaktInfo, status: "aktiv" };
console.log(vollstaendigesProfil);
// { name: "Lisa", alter: 28, email: "lisa@dev.io", stadt: "Hamburg", status: "aktiv" }
// 🌟 Rest: Eigenschaften separieren:
const { email, ...restlicheDaten } = vollstaendigesProfil;
console.log(email); // "lisa@dev.io"
console.log(restlicheDaten); // { name: "Lisa", alter: 28, stadt: "Hamburg", status: "aktiv" }</code></pre>
<div class="box box-analogie">
<div class="box-title">🧰 Didaktische Analogie: Die Werkzeugkiste &amp; Auspack-Station</div>
<p>Ein Objekt ist wie eine beschriftete <strong>Werkzeugkiste</strong>. Destructuring ist das gezielte Herausgreifen von genau dem Werkzeug (z.&nbsp;B. Hammer und Zange), das du jetzt benötigst, ohne die ganze Kiste herumtragen zu müssen. Der Spread-Operator (<code>...</code>) ist wie das Umschütten zweier Kisten in einen neuen Koffer, ohne den Originalinhalt zu beschädigen.</p>
</div>
</section>
<!-- 4. CHEAT-SHEET -->
<section class="card">
<h2>📑 4. Cheat-Sheet: Object &amp; Destructuring Methoden</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
<thead>
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
<th style="padding: 10px;">Syntax / Methode</th>
<th style="padding: 10px;">Beispiel</th>
<th style="padding: 10px;">Beschreibung</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>{ a, b }</code></td>
<td style="padding: 10px;"><code>const obj = { name, email };</code></td>
<td style="padding: 10px;">Property Shorthand</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>const { k } = obj</code></td>
<td style="padding: 10px;"><code>const { name, rolle = "user" } = u;</code></td>
<td style="padding: 10px;">Object Destructuring mit Default-Wert</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>{ ...obj, neu }</code></td>
<td style="padding: 10px;"><code>const klon = { ...original, id: 42 };</code></td>
<td style="padding: 10px;">Spread-Operator zum Klonen &amp; Erweitern</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>Object.keys(obj)</code></td>
<td style="padding: 10px;"><code>Object.keys({ a: 1, b: 2 }) // ["a", "b"]</code></td>
<td style="padding: 10px;">Array aller Eigenschaftsnamen (Keys)</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>Object.values(obj)</code></td>
<td style="padding: 10px;"><code>Object.values({ a: 1, b: 2 }) // [1, 2]</code></td>
<td style="padding: 10px;">Array aller Werte (Values)</td>
</tr>
<tr>
<td style="padding: 10px;"><code>Object.entries(obj)</code></td>
<td style="padding: 10px;"><code>Object.entries({ a: 1 }) // [["a", 1]]</code></td>
<td style="padding: 10px;">Array aus <code>[key, value]</code>-Paaren</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 5. AUFGABEN CHECKLISTE -->
<section class="card">
<h2>🎯 Aufgaben &amp; Checkliste in <code>aufgabe.js</code></h2>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t1">
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>erstelleBenutzer(name, email, rolle = "user")</code> mit Property Shorthand implementieren.</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t2">
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>extrahiereDetails(benutzerObjekt)</code> mit Object Destructuring formatieren (<code>"${name} (${email})"</code>).</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t3">
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>fuegeAdresseHinzu(benutzerObjekt, adresseObjekt)</code> mit Spread-Operator (<code>{ ...benutzer, ...adresse }</code>) ohne Mutation bauen.</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t4">
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>zaehleEigenschaften(objekt)</code> mit <code>Object.keys().length</code> schreiben.</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t5">
<label class="task-label" for="t5"><strong>TODO 5:</strong> <code>berechneWarenkorbGesamtwert(warenkorbItems)</code> mit Destructuring im Loop / reduce umsetzen.</label>
</div>
<div class="box box-tipp" style="margin-top: 20px;">
<div class="box-title">💡 Tipp für Teilziel 5</div>
<p>Du kannst Destructuring direkt in der Funktionssignatur von <code>reduce</code> oder im Schleifenkopf nutzen:
<code>items.reduce((sum, { preis = 0, anzahl = 1 }) =&gt; sum + (preis * anzahl), 0)</code></p>
</div>
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=03_objekte_und_destructuring" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen &rarr;</a>
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">&larr; Zurück zu JS 02</a>
<a href="../04_dom_und_events/index.html" class="btn btn-secondary">Weiter zu JS 04 &rarr;</a>
</div>
</section>
</main>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
/**
* 🌟 MUSTERLÖSUNG: JS 03 – OBJEKTE & DESTRUCTURING 🌟
* ==================================================
*/
function erstelleBenutzer(name, email, rolle = "user") {
return { name, email, rolle };
}
function extrahiereDetails(benutzerObjekt) {
const { name, email } = benutzerObjekt;
return `${name} (${email})`;
}
function fuegeAdresseHinzu(benutzerObjekt, adresseObjekt) {
return { ...benutzerObjekt, ...adresseObjekt };
}
function zaehleEigenschaften(objekt) {
if (!objekt || typeof objekt !== "object") return 0;
return Object.keys(objekt).length;
}
function berechneWarenkorbGesamtwert(warenkorbItems) {
if (!Array.isArray(warenkorbItems) || warenkorbItems.length === 0) return 0;
return warenkorbItems.reduce((gesamt, { preis = 0, anzahl = 1 }) => gesamt + preis * anzahl, 0);
}

View file

@ -0,0 +1,83 @@
// 🧪 JS 03: Testsuite für Objekte, Destructuring & Rest/Spread
// Test 1: erstelleBenutzer
const user1 = erstelleBenutzer("Max Mustermann", "max@beispiel.de");
assert.deepStrictEqual(
user1,
{ name: "Max Mustermann", email: "max@beispiel.de", rolle: "user" },
"erstelleBenutzer() sollte Standard-Rolle 'user' und korrekte Attribute besitzen"
);
const adminUser = erstelleBenutzer("Alice Admin", "alice@corp.de", "admin");
assert.deepStrictEqual(
adminUser,
{ name: "Alice Admin", email: "alice@corp.de", rolle: "admin" },
"erstelleBenutzer() sollte benutzerdefinierte Rolle 'admin' unterstützen"
);
// Test 2: extrahiereDetails
const details1 = extrahiereDetails({ name: "Anna Schmidt", email: "anna@web.de", rolle: "user" });
assert.strictEqual(
details1,
"Anna Schmidt (anna@web.de)",
"extrahiereDetails() sollte 'Name (Email)' zurückgeben"
);
const details2 = extrahiereDetails({ name: "Bob", email: "bob@dev.io", alter: 30 });
assert.strictEqual(
details2,
"Bob (bob@dev.io)",
"extrahiereDetails() sollte auch mit zusätzlichen Objekt-Eigenschaften funktionieren"
);
// Test 3: fuegeAdresseHinzu & Immutability Check
const basisBenutzer = { name: "Sarah", email: "sarah@mail.de" };
const adresse = { stadt: "Hamburg", plz: "20095" };
const kombiniert = fuegeAdresseHinzu(basisBenutzer, adresse);
assert.deepStrictEqual(
kombiniert,
{ name: "Sarah", email: "sarah@mail.de", stadt: "Hamburg", plz: "20095" },
"fuegeAdresseHinzu() sollte Benutzer und Adresse zu einem neuen Objekt verschmelzen"
);
assert.strictEqual(
basisBenutzer.stadt,
undefined,
"fuegeAdresseHinzu() darf das Originalobjekt nicht mutieren (Immutability)"
);
// Test 4: zaehleEigenschaften
assert.strictEqual(zaehleEigenschaften({ a: 1, b: 2, c: 3 }), 3, "zaehleEigenschaften({a:1, b:2, c:3}) sollte 3 ergeben");
assert.strictEqual(zaehleEigenschaften({}), 0, "zaehleEigenschaften({}) für leeres Objekt sollte 0 sein");
assert.strictEqual(
zaehleEigenschaften({ id: 101, title: "Laptop", preis: 999, verfuegbar: true }),
4,
"zaehleEigenschaften() sollte 4 Eigenschaften korrekt zählen"
);
// Test 5: berechneWarenkorbGesamtwert
const warenkorb = [
{ titel: "Buch", preis: 20, anzahl: 2 },
{ titel: "Maus", preis: 15, anzahl: 3 },
{ titel: "Kabel", preis: 5, anzahl: 1 }
];
assert.strictEqual(
berechneWarenkorbGesamtwert(warenkorb),
90,
"berechneWarenkorbGesamtwert() sollte 20*2 + 15*3 + 5*1 = 90 berechnen"
);
const warenkorbMitDefault = [{ titel: "Monitor", preis: 199 }];
assert.strictEqual(
berechneWarenkorbGesamtwert(warenkorbMitDefault),
199,
"berechneWarenkorbGesamtwert() sollte anzahl = 1 als Fallback nutzen"
);
assert.strictEqual(
berechneWarenkorbGesamtwert([]),
0,
"berechneWarenkorbGesamtwert([]) für leeren Warenkorb sollte 0 sein"
);
console.log("✅ Alle 10 Tests in JS 03 erfolgreich bestanden!");

View file

@ -0,0 +1,143 @@
# JS 04: DOM-Manipulation & Events 🎭
Willkommen zu **Modul 04** des JavaScript-Kurses!
Das **Document Object Model (DOM)** ist die Schnittstelle zwischen JavaScript und der sichtbaren HTML-Struktur im Webbrowser. Durch die Kombination aus DOM-Selektoren, Manipulation und Event-Listenern hauchst du statischen Webseiten echtes Leben ein und machst sie interaktiv.
---
## 💡 1. Das Wichtigste in Kürze
### A. Was ist das DOM?
Der Browser parst HTML-Code und baut daraus einen hierarchischen Baum aus JavaScript-Objekten (*DOM Tree*):
```
document
│
<html>
┌───┴───┐
<head> <body>
│ ┌───┴────────┐
<title> <h1> <div> (Container)
┌───┴───┐
<button> <ul>
│
<li>
```
Jeder HTML-Tag wird zu einem **DOM-Element (Node)** mit Eigenschaften und Methoden, die du in JavaScript zur Laufzeit auslesen und verändern kannst.
---
### B. Elemente selektieren
Moderne Selektoren nutzen dieselbe Syntax wie CSS:
```javascript
// Einzelnes Element selektieren (erstes Vorkommen):
const ueberschrift = document.querySelector("h1");
const speicherBtn = document.querySelector("#save-btn");
const erstesItem = document.querySelector(".liste-item");
// Mehrere Elemente selektieren (NodeList / Collection):
const alleButtons = document.querySelectorAll(".btn");
alleButtons.forEach(btn => console.log(btn.textContent));
```
---
### C. Element-Inhalte & Klassen manipulieren
```javascript
const box = document.querySelector("#info-box");
// 1. Textinhalt sicher ändern (schützt vor XSS-Sicherheitslücken):
box.textContent = "Neuer sicherer Text";
// 2. CSS-Klassen verwalten mit classList:
box.classList.add("aktiv"); // Klasse hinzufügen
box.classList.remove("inaktiv"); // Klasse entfernen
box.classList.toggle("sichtbar"); // Hinzufügen falls fehlt, sonst entfernen
console.log(box.classList.contains("aktiv")); // true oder false
```
---
### D. Event-Listener (Ereignis-Steuerung)
Mit `addEventListener()` reagiert deine Webanwendung auf Benutzerinteraktionen wie Klicks, Tastatureingaben oder Formulare:
```javascript
const button = document.querySelector("#klick-mich");
button.addEventListener("click", (event) => {
console.log("Button wurde angeklickt!", event.target);
button.textContent = "Erfolgreich geklickt! 🎉";
});
```
---
### E. Dynamische Elemente erstellen
Du kannst neue HTML-Elemente dynamisch zur Laufzeit erzeugen und in den DOM-Baum einhängen:
```javascript
// 1. Element im Speicher erzeugen:
const neuesLi = document.createElement("li");
neuesLi.textContent = "Neuer dynamischer Eintrag";
neuesLi.classList.add("eintrag");
// 2. In den DOM-Baum an ein Elternelement anfügen:
const liste = document.querySelector("#aufgaben-liste");
liste.appendChild(neuesLi);
```
---
## 🎼 Die didaktische Analogie: "Der Dirigent des Web-Orchesters"
- **Das HTML-Dokument als Partitur:**
HTML liefert den statischen Notentext und die Struktur des Musikstücks.
- **Der Browser als Orchester:**
Die Musiker (DOM-Nodes) sitzen bereit, um Töne und Farben darzustellen.
- **JavaScript als Dirigent:**
- **Selektoren (`querySelector`):** Der Dirigent schaut gezielt zu den Streichern oder Bläsern.
- **Manipulation (`textContent`, `classList`):** Mit dem Taktstock gibt der Dirigent Anweisungen für Lautstärke, Helligkeit oder Text.
- **Events (`addEventListener`):** Der Dirigent wartet auf den Einsatz oder den Applaus des Publikums und reagiert dynamisch.
---
## 🏗️ Headless- & Isolierte Logik (Testbare Architektur)
In moderner Web-Entwicklung (wie React, Vue oder Node.js) trennen professionelle Entwickler reine **Berechnungs- und Zustandslogik** von direkten DOM-Zugriffen.
Dadurch können Funktionen wie `aktualisiereZaehlerAnzeige` oder `filtereElementeNachKlasse` sowohl im echten Browser als auch in **Headless-Umgebungen (z.B. Unittests im Web-Worker / CI-Runner)** blitzschnell und ohne echten Monitor getestet werden!
---
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
Öffne `aufgabe.js` und löse die 4 Aufgaben:
1. **TODO 1:** `erzeugeButtonElement(text, cssKlasse, clickCallback)`
Erstelle einen Button (`document.createElement("button")`), setze den Text, füge optional die CSS-Klasse hinzu und binde den Click-Callback per `addEventListener` an.
2. **TODO 2:** `erstelleListenEintrag(titel, istErledigt = false)`
Erstelle ein `<li>`-Element mit dem Titel und füge die Klasse `"erledigt"` hinzu, falls `istErledigt === true` ist.
3. **TODO 3:** `filtereElementeNachKlasse(elementListe, cssKlasse)`
Filtere ein Array von DOM-Elementen nach `element.classList.contains(cssKlasse)`.
4. **TODO 4:** `aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10)`
Berechne das Zustands-Objekt für eine Zähler-Komponente:
```javascript
{
text: `Zähler: ${aktuellerWert} / ${schwellenwert}`,
wert: aktuellerWert,
istKritisch: aktuellerWert >= schwellenwert,
statusKlasse: aktuellerWert >= schwellenwert ? "status-kritisch" : "status-normal"
}
```
---
## 🧪 Tests ausführen
Starte die Tests im Test-Runner oder in der Web-IDE. Sobald alle Tests grün sind, hast du DOM-Manipulation & Events gemeistert!

View file

@ -0,0 +1,48 @@
/**
* 🌐 JS 04: DOM-MANIPULATION & EVENTS 🌐
* ========================================
* Interaktive Webseiten & Event-Driven Architecture in JavaScript.
*/
// 🎯 TEILZIEL 1 (TODO 1): erzeugeButtonElement(text, cssKlasse, clickCallback)
// Erzeuge ein Button-Element mit document.createElement("button").
// 1. Setze textContent auf 'text'.
// 2. Füge 'cssKlasse' über classList.add(cssKlasse) hinzu (falls cssKlasse übergeben wurde).
// 3. Registriere 'clickCallback' mit button.addEventListener("click", clickCallback) (falls clickCallback eine Funktion ist).
// Rückgabe: Das fertige Button-Element
function erzeugeButtonElement(text, cssKlasse, clickCallback) {
// TODO: Button erstellen, Text setzen, Klasse hinzufügen, Event-Listener registrieren
return null;
}
// 🎯 TEILZIEL 2 (TODO 2): erstelleListenEintrag(titel, istErledigt = false)
// Erzeuge ein <li>-Element mit document.createElement("li").
// 1. Setze textContent auf 'titel'.
// 2. Wenn istErledigt === true ist, füge die CSS-Klasse "erledigt" hinzu (classList.add("erledigt")).
// Rückgabe: Das erstellte <li>-Element
function erstelleListenEintrag(titel, istErledigt = false) {
// TODO: <li> erzeugen, Text setzen, optional Klasse "erledigt" hinzufügen
return null;
}
// 🎯 TEILZIEL 3 (TODO 3): filtereElementeNachKlasse(elementListe, cssKlasse)
// Filtert ein Array oder eine NodeList von Elementen.
// Rückgabe: Ein neues Array, das nur Elemente enthält, bei denen element.classList.contains(cssKlasse) true ist.
function filtereElementeNachKlasse(elementListe, cssKlasse) {
// TODO: Array.from(elementListe).filter(...) mit classList.contains(cssKlasse)
return [];
}
// 🎯 TEILZIEL 4 (TODO 4): aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10)
// Berechnet den Status einer Zähleranzeige (Headless-UI State Pattern):
// Rückgabe-Objekt:
// {
// text: `Zähler: ${aktuellerWert} / ${schwellenwert}`,
// wert: aktuellerWert,
// istKritisch: aktuellerWert >= schwellenwert,
// statusKlasse: aktuellerWert >= schwellenwert ? "status-kritisch" : "status-normal"
// }
function aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10) {
// TODO: Zählerstatus berechnen und Objekt zurückgeben
return null;
}

View file

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS 04: DOM-Manipulation &amp; Events</title>
<link rel="stylesheet" href="../../../assets/style.css">
</head>
<body>
<div class="app-layout">
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🌐 JavaScript DOM &amp; Web APIs</div>
<h1>JS 04: DOM-Manipulation &amp; Events</h1>
<p>Lerne dynamische Webseiten zu bauen mit dem Document Object Model (DOM), Event-Listenern und UI-Zustandslogik.</p>
</div>
</header>
<main class="container">
<!-- 1. THEORIE: DOM BAUM & SELEKTOREN -->
<section class="card">
<h2>🌳 1. Der DOM-Baum &amp; Selektoren</h2>
<p>Der Browser übersetzt HTML in eine Baumstruktur von Objekten (den <strong>DOM-Baum</strong>). Mit modernen Selektoren greifst du gezielt auf jedes Element zu:</p>
<div style="background: var(--bg-subtle); padding: 16px; border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.4; margin-bottom: 16px; border: 1px solid var(--border-color);">
document<br>
└── &lt;html&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;├── &lt;head&gt; (title, meta, link)<br>
&nbsp;&nbsp;&nbsp;&nbsp;└── &lt;body&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── &lt;h1 id="titel"&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── &lt;button class="btn"&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;└── &lt;ul id="aufgaben"&gt; ── &lt;li class="erledigt"&gt;
</div>
<pre><code>// 🎯 Einzelnes Element mit CSS-Selektor ansprechen:
const titel = document.querySelector("#titel");
const button = document.querySelector(".btn-primary");
// 🎯 Alle passenden Elemente als NodeList abfragen:
const alleItems = document.querySelectorAll("ul#aufgaben > li");
alleItems.forEach(item => console.log(item.textContent));</code></pre>
</section>
<!-- 2. THEORIE: ELEMENTE MANIPULIEREN -->
<section class="card">
<h2>🎨 2. Elemente manipulieren &amp; stylen</h2>
<p>Über Eigenschaften wie <code>textContent</code> und <code>classList</code> veränderst du Texte und Klassen zur Laufzeit:</p>
<pre><code>const karte = document.querySelector(".info-karte");
// 🔒 Sicherer Text (verhindert Cross-Site-Scripting / XSS):
karte.textContent = "Aktualisierter Status: Online";
// 🏷️ CSS-Klassen verwalten mit classList:
karte.classList.add("aktiv"); // Klasse hinzufügen
karte.classList.remove("inaktiv"); // Klasse entfernen
karte.classList.toggle("hervorgehoben"); // Ein- oder ausschalten
console.log(karte.classList.contains("aktiv")); // true</code></pre>
</section>
<!-- 3. THEORIE: EVENTS -->
<section class="card">
<h2>⚡ 3. Event-Driven Architecture (Reaktivität)</h2>
<p>Mit <code>addEventListener()</code> reagiert deine Website sofort auf Aktionen des Nutzers (Klicks, Tastenanschläge, Formularabsendungen):</p>
<pre><code>const saveBtn = document.querySelector("#btn-save");
saveBtn.addEventListener("click", (event) => {
console.log("Button geklickt! Ziel-Element:", event.target);
saveBtn.textContent = "Gespeichert! ✅";
saveBtn.classList.add("btn-success");
});</code></pre>
<div class="box box-analogie">
<div class="box-title">🎼 Didaktische Analogie: Der Dirigent des Web-Orchesters</div>
<p>HTML ist die <strong>Partitur</strong>, der Browser das <strong>Orchester</strong> und dein JavaScript-Code der <strong>Dirigent</strong>: Selektoren sind der gezielte Blick zu den Instrumentengruppen, DOM-Manipulation ist die Taktstockbewegung für Lautstärke oder Klang, und Event-Listener sind das aufmerksame Lauschen auf den richtigen Takt und Applaus.</p>
</div>
</section>
<!-- 4. CHEAT-SHEET -->
<section class="card">
<h2>📑 4. Cheat-Sheet: DOM-Methoden &amp; Events</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
<thead>
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
<th style="padding: 10px;">Methode / Eigenschaft</th>
<th style="padding: 10px;">Beispiel</th>
<th style="padding: 10px;">Wirkung</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>document.querySelector(sel)</code></td>
<td style="padding: 10px;"><code>document.querySelector(".btn")</code></td>
<td style="padding: 10px;">Findet das erste passende Element</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>document.querySelectorAll(sel)</code></td>
<td style="padding: 10px;"><code>document.querySelectorAll("li")</code></td>
<td style="padding: 10px;">Findet alle passenden Elemente</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>document.createElement(tag)</code></td>
<td style="padding: 10px;"><code>const btn = document.createElement("button")</code></td>
<td style="padding: 10px;">Erstellt ein neues Element im Speicher</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>element.textContent</code></td>
<td style="padding: 10px;"><code>el.textContent = "Hallo Welt"</code></td>
<td style="padding: 10px;">Liest oder setzt reinen Text</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 10px;"><code>element.classList.add/remove/contains</code></td>
<td style="padding: 10px;"><code>el.classList.contains("aktiv")</code></td>
<td style="padding: 10px;">Manipuliert und prüft CSS-Klassen</td>
</tr>
<tr>
<td style="padding: 10px;"><code>element.addEventListener(ev, fn)</code></td>
<td style="padding: 10px;"><code>el.addEventListener("click", () => ...)</code></td>
<td style="padding: 10px;">Registriert Callback bei Ereignis</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 5. AUFGABEN CHECKLISTE -->
<section class="card">
<h2>🎯 Aufgaben &amp; Checkliste in <code>aufgabe.js</code></h2>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t1">
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>erzeugeButtonElement(text, cssKlasse, clickCallback)</code> mit <code>createElement</code> &amp; <code>addEventListener</code> schreiben.</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t2">
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>erstelleListenEintrag(titel, istErledigt = false)</code> mit Klasse <code>"erledigt"</code> erstellen.</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t3">
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>filtereElementeNachKlasse(elementListe, cssKlasse)</code> mit <code>classList.contains</code> filtern.</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t4">
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10)</code> für testbare State-Logik umsetzen.</label>
</div>
<div class="box box-tipp" style="margin-top: 20px;">
<div class="box-title">💡 Architektur-Tipp: Headless-UI &amp; Unittests</div>
<p>Indem du DOM-Helfer und Zustandslogik in modulare Funktionen aufteilst, kann dein Code sowohl im Webbrowser als auch in isolierten Testumgebungen (wie Web-Workern und CI-Pipelines) zuverlässig getestet werden.</p>
</div>
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=04_dom_und_events" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen &rarr;</a>
<a href="../03_objekte_und_destructuring/index.html" class="btn btn-secondary">&larr; Zurück zu JS 03</a>
<a href="../05_callbacks_und_promises/index.html" class="btn btn-secondary">Weiter zu JS 05 &rarr;</a>
</div>
</section>
</main>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,40 @@
/**
* 🌟 MUSTERLÖSUNG: JS 04 – DOM & EVENTS 🌟
* ========================================
*/
function erzeugeButtonElement(text, cssKlasse, clickCallback) {
const button = document.createElement("button");
button.textContent = text;
if (cssKlasse) {
button.classList.add(cssKlasse);
}
if (typeof clickCallback === "function") {
button.addEventListener("click", clickCallback);
}
return button;
}
function erstelleListenEintrag(titel, istErledigt = false) {
const li = document.createElement("li");
li.textContent = titel;
if (istErledigt) {
li.classList.add("erledigt");
}
return li;
}
function filtereElementeNachKlasse(elementListe, cssKlasse) {
const arr = Array.isArray(elementListe) ? elementListe : Array.from(elementListe || []);
return arr.filter(el => el && el.classList && el.classList.contains(cssKlasse));
}
function aktualisiereZaehlerAnzeige(aktuellerWert, schwellenwert = 10) {
const istKritisch = aktuellerWert >= schwellenwert;
return {
text: `Zähler: ${aktuellerWert} / ${schwellenwert}`,
wert: aktuellerWert,
istKritisch,
statusKlasse: istKritisch ? "status-kritisch" : "status-normal"
};
}

View file

@ -0,0 +1,127 @@
// 🧪 JS 04: Testsuite für DOM-Manipulation & Events
// Mock-DOM Umgebung für Headless / Web-Worker Ausführung
if (typeof document === "undefined") {
globalThis.document = {
createElement(tagName) {
const classes = new Set();
const listeners = {};
const el = {
tagName: String(tagName).toUpperCase(),
textContent: "",
innerHTML: "",
get className() {
return Array.from(classes).join(" ");
},
set className(val) {
classes.clear();
if (val) String(val).trim().split(/\s+/).forEach(c => c && classes.add(c));
},
classList: {
add(...tokens) {
tokens.forEach(t => t && classes.add(String(t)));
},
remove(...tokens) {
tokens.forEach(t => classes.delete(String(t)));
},
toggle(token) {
if (classes.has(token)) {
classes.delete(token);
return false;
} else {
classes.add(token);
return true;
}
},
contains(token) {
return classes.has(token);
}
},
addEventListener(event, callback) {
if (!listeners[event]) listeners[event] = [];
listeners[event].push(callback);
},
dispatchEvent(eventObj) {
const type = typeof eventObj === "string" ? eventObj : eventObj.type;
const list = listeners[type] || [];
const ev = typeof eventObj === "string" ? { type, target: this } : eventObj;
list.forEach(cb => cb(ev));
},
click() {
this.dispatchEvent({ type: "click", target: this });
}
};
return el;
}
};
}
// Test 1: erzeugeButtonElement
let clicked = false;
const btn1 = erzeugeButtonElement("Speichern", "btn-primary", () => { clicked = true; });
assert.ok(btn1, "erzeugeButtonElement() sollte ein Element zurückgeben");
assert.strictEqual(btn1.textContent, "Speichern", "Button textContent sollte 'Speichern' sein");
assert.ok(btn1.classList.contains("btn-primary"), "Button sollte die Klasse 'btn-primary' besitzen");
if (typeof btn1.click === "function") {
btn1.click();
} else if (typeof btn1.dispatchEvent === "function") {
btn1.dispatchEvent({ type: "click" });
}
assert.strictEqual(clicked, true, "Button Click-Callback sollte beim Klick ausgelöst werden");
// Test 1b: erzeugeButtonElement ohne optionale Klasse/Callback
const btnSimple = erzeugeButtonElement("Abbrechen");
assert.strictEqual(btnSimple.textContent, "Abbrechen", "Einfacher Button ohne Parameter sollte funktionieren");
// Test 2: erstelleListenEintrag
const liOffen = erstelleListenEintrag("Milch kaufen", false);
assert.ok(liOffen, "erstelleListenEintrag() sollte ein <li> Element zurückgeben");
assert.strictEqual(liOffen.textContent, "Milch kaufen", "Listeneintrag-Text sollte 'Milch kaufen' sein");
assert.strictEqual(liOffen.classList.contains("erledigt"), false, "Unerledigter Eintrag darf nicht die Klasse 'erledigt' haben");
const liErledigt = erstelleListenEintrag("Post abholen", true);
assert.strictEqual(liErledigt.textContent, "Post abholen", "Erledigter Eintrag Text sollte stimmen");
assert.strictEqual(liErledigt.classList.contains("erledigt"), true, "Erledigter Eintrag muss die Klasse 'erledigt' besitzen");
// Test 3: filtereElementeNachKlasse
const el1 = document.createElement("div");
el1.classList.add("aktiv");
const el2 = document.createElement("div");
el2.classList.add("inaktiv");
const el3 = document.createElement("div");
el3.classList.add("aktiv");
el3.classList.add("hervorgehoben");
const gefiltertAktiv = filtereElementeNachKlasse([el1, el2, el3], "aktiv");
assert.strictEqual(gefiltertAktiv.length, 2, "filtereElementeNachKlasse() sollte 2 aktive Elemente finden");
assert.strictEqual(gefiltertAktiv[0], el1, "Erstes gefiltertes Element sollte el1 sein");
assert.strictEqual(gefiltertAktiv[1], el3, "Zweites gefiltertes Element sollte el3 sein");
const gefiltertHervorgehoben = filtereElementeNachKlasse([el1, el2, el3], "hervorgehoben");
assert.strictEqual(gefiltertHervorgehoben.length, 1, "Sollte 1 hervorgehobenes Element finden");
// Test 4: aktualisiereZaehlerAnzeige
const statusNormal = aktualisiereZaehlerAnzeige(5, 10);
assert.deepStrictEqual(
statusNormal,
{ text: "Zähler: 5 / 10", wert: 5, istKritisch: false, statusKlasse: "status-normal" },
"aktualisiereZaehlerAnzeige(5, 10) sollte Normalstatus liefern"
);
const statusKritisch = aktualisiereZaehlerAnzeige(10, 10);
assert.deepStrictEqual(
statusKritisch,
{ text: "Zähler: 10 / 10", wert: 10, istKritisch: true, statusKlasse: "status-kritisch" },
"aktualisiereZaehlerAnzeige(10, 10) bei Erreichen des Schwellenwerts sollte kritisch sein"
);
const statusUeberlimit = aktualisiereZaehlerAnzeige(15, 10);
assert.strictEqual(statusUeberlimit.istKritisch, true, "Überlimit sollte istKritisch=true haben");
assert.strictEqual(statusUeberlimit.statusKlasse, "status-kritisch", "Überlimit sollte 'status-kritisch' haben");
const statusDefault = aktualisiereZaehlerAnzeige(3);
assert.strictEqual(statusDefault.text, "Zähler: 3 / 10", "Standard-Schwellenwert sollte 10 sein");
console.log("✅ Alle 14 Tests in JS 04 erfolgreich bestanden!");

View file

@ -0,0 +1,131 @@
# 🌐 JS 05: Callbacks, Event Loop & Promises
Willkommen zu **Modul 05** des JavaScript-Kurses! In diesem Kapitel lernst du das Herzstück der modernen Web-Entwicklung kennen: **Asynchrone Programmierung mit Promises**.
---
## 📖 1. Synchron vs. Asynchron & Der Event Loop
JavaScript führt Code in einer einzigen Hauptausführungsschleife aus (**Single-Threaded**).
- **Synchrone Ausführung (Blockierend):** Jede Codezeile wird nacheinander ausgeführt. Dauert eine Operation lange (z. B. ein schwerer Rechenschritt oder ein synchrones Warten), friert das gesamte Programm (und die Webseite!) ein.
- **Asynchrone Ausführung (Nicht-blockierend):** Zeitintensive Aufgaben (z. B. Netzwerk-Anfragen, Timer oder Dateizugriffe) werden im Hintergrund angestoßen. JavaScript läuft währenddessen sofort weiter. Sobald das Ergebnis vorliegt, wird ein Callback in die Warteschlange gelegt und vom **Event Loop** abgearbeitet.
```javascript
console.log("1. Bestellung aufgeben");
setTimeout(() => {
console.log("3. Essen ist fertig!"); // Asynchroner Callback nach 1000ms
}, 1000);
console.log("2. An den Tisch setzen und plaudern");
// Ausgabe-Reihenfolge: 1 -> 2 -> 3
```
---
## 🍔 2. Die Restaurant-Pieper-Analogie
Stell dir vor, du bestellst Burger in einem Selbstbedienungs-Restaurant:
1. **Bestellung:** Du gehst an die Kasse und bestellst dein Menü.
2. **Der Pieper (`Promise` im Zustand `pending`):** Die Bedienung gibt dir sofort einen elektronischen Pieper in die Hand. Du musst *nicht* stur am Tresen stehen bleiben und die Schlange blockieren.
3. **Nicht-blockierendes Warten:** Du setzt dich an deinen Tisch, surfst am Smartphone oder unterhältst dich mit Freunden (JavaScript Event Loop verarbeitet andere Ereignisse).
4. **Erfolg (`resolve` -> `fulfilled`):** Der Pieper vibriert und leuchtet grün! Du holst dein frisches Essen ab (`.then(essen => ...)`).
5. **Fehler (`reject` -> `rejected`):** Der Pieper piept rot und die Bedienung ruft: *"Die Burger-Patties sind leider aus!"* (`.catch(fehler => ...)`).
---
## ⏳ 3. Was ist ein Promise?
Ein **Promise** (deutsch: *Versprechen*) ist ein Objekt, das einen Wert repräsentiert, der jetzt, in der Zukunft oder nie verfügbar ist.
### Die 3 Zustände eines Promise:
```
┌───> fulfilled (erfolgreich gelöst) ──> .then(wert)
pending ──────┤
(schwebend) └───> rejected (fehlgeschlagen) ──> .catch(fehler)
```
1. **`pending` (schwebend):** Die asynchrone Operation läuft noch. Weder Erfolg noch Fehler.
2. **`fulfilled` (erfüllt):** Die Operation war erfolgreich, `resolve(wert)` wurde aufgerufen.
3. **`rejected` (abgewiesen):** Ein Fehler ist aufgetreten, `reject(error)` wurde aufgerufen.
---
## 🛠️ 4. Eigene Promises erstellen
Ein Promise wird mit dem Konstruktor `new Promise(executor)` erzeugt. Die Executor-Funktion erhält zwei Parameter: `resolve` und `reject`.
```javascript
function simuliereDatenAbruf(erfolgreich) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (erfolgreich) {
resolve({ id: 1, name: "Max Mustermann" });
} else {
reject(new Error("Netzwerk-Timeout beim Laden!"));
}
}, 500);
});
}
```
---
## 🔗 5. Promise-Chaining: `.then()`, `.catch()`, `.finally()`
Promises ermöglichen sauberes Verketten ohne verschachtelte Callback-Pyramiden (*Callback Hell*):
```javascript
simuliereDatenAbruf(true)
.then((daten) => {
console.log("Erhaltene Daten:", daten.name);
return daten.id; // Rückgabewert wird als nächstes Promise weitergegeben
})
.then((userId) => {
console.log("Lade Details für ID:", userId);
})
.catch((err) => {
console.error("Ein Fehler ist aufgetreten:", err.message);
})
.finally(() => {
console.log("Lade-Indikator ausblenden (wird immer ausgeführt)");
});
```
---
## ⚡ 6. Parallele Ausführung mit `Promise.all()` und `Promise.race()`
### `Promise.all([p1, p2, ...])`
Führt mehrere Promises parallel aus und wartet, bis **alle** erfolgreich aufgelöst wurden.
- Gibt ein Array mit allen Einzelergebnissen `[res1, res2, ...]` zurück.
- **Fail-Fast:** Bricht sofort ab, sobald *auch nur ein einzelnes* Promise rejected!
```javascript
const p1 = fetchUser();
const p2 = fetchPosts();
Promise.all([p1, p2])
.then(([user, posts]) => {
console.log("User & Posts gleichzeitig geladen:", user, posts);
})
.catch(err => console.error("Mindestens eine Quelle schlug fehl:", err));
```
### `Promise.race([p1, p2, ...])`
Liefert das Ergebnis des **allerschnellsten** Promise (egal ob `resolve` oder `reject`). Ideal z. B. für Timeouts.
---
## 🎯 7. Deine Aufgaben in `aufgabe.js`
1. **`verzoegereAusfuehrung(ms, rueckgabeWert)`**:
Erzeuge ein Promise, das nach `ms` Millisekunden mittels `setTimeout` den `rueckgabeWert` auflöst.
2. **`pruefeZahlGroesserNull(zahl)`**:
Prüfe die Zahl: Resolve bei `zahl > 0`, Reject mit `new Error("Zahl muss größer als 0 sein")` bei `<= 0`.
3. **`ladeBenutzerProfil(userId)`**:
Asynchrones Profil-Laden: Resolve mit `{ id: userId, name: "Benutzer_" + userId, status: "aktiv" }` bei `userId > 0`, sonst Reject mit `new Error("Ungültige User-ID")`.
4. **`ladeAlleDatenQuellen(quelleA, quelleB)`**:
Nutze `Promise.all([quelleA, quelleB])`, um zwei asynchrone Datenströme parallel zu aggregieren.

View file

@ -0,0 +1,44 @@
/**
* 🌐 JS 05: CALLBACKS, EVENT LOOP & PROMISES 🌐
* =============================================
* Lerne die Grundlagen der asynchronen Programmierung in JavaScript:
* Callbacks, der Event Loop und Promises für nicht-blockierenden Code.
*/
// 🎯 TEILZIEL 1 (TODO 1): Schreibe verzoegereAusfuehrung(ms, rueckgabeWert)
// Gibt ein neues Promise zurück, das nach `ms` Millisekunden mit `rueckgabeWert` aufgelöst (resolved) wird.
// Nutze dafür setTimeout(callback, ms).
function verzoegereAusfuehrung(ms, rueckgabeWert) {
// TODO: Gib ein new Promise((resolve) => ...) zurück
return null;
}
// 🎯 TEILZIEL 2 (TODO 2): Schreibe pruefeZahlGroesserNull(zahl)
// Gibt ein Promise zurück:
// - Wenn zahl > 0: resolve mit zahl
// - Wenn zahl <= 0 oder keine Zahl: reject mit einem Error("Zahl muss größer als 0 sein")
function pruefeZahlGroesserNull(zahl) {
// TODO: Gib ein Promise zurück, das resolve() oder reject() aufruft
return null;
}
// 🎯 TEILZIEL 3 (TODO 3): Schreibe ladeBenutzerProfil(userId)
// Simuliert das asynchrone Laden eines Benutzerprofils:
// - Wenn userId ein positiver Wert (> 0) ist:
// Gibt ein Promise zurück, das nach kurzer Zeit (z.B. 10ms) ein Objekt auflöst:
// { id: userId, name: `Benutzer_${userId}`, status: "aktiv" }
// - Wenn userId <= 0 oder ungültig:
// Gibt ein abgewiesenes Promise (reject) mit Error("Ungültige User-ID") zurück.
function ladeBenutzerProfil(userId) {
// TODO: Implementiere ladeBenutzerProfil mit Promise
return null;
}
// 🎯 TEILZIEL 4 (TODO 4): Schreibe ladeAlleDatenQuellen(quelleA, quelleB)
// Nimmt zwei Promises quelleA und quelleB entgegen und nutzt Promise.all([...]),
// um beide Datenquellen parallel abzuwarten.
// Gibt das Promise von Promise.all zurück, das ein Array [ergebnisA, ergebnisB] liefert.
function ladeAlleDatenQuellen(quelleA, quelleB) {
// TODO: Nutze Promise.all([quelleA, quelleB])
return null;
}

View file

@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS 05: Callbacks, Event Loop &amp; Promises</title>
<link rel="stylesheet" href="../../../assets/style.css">
<style>
.promise-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin: 24px 0;
flex-wrap: wrap;
}
.state-box {
padding: 16px 20px;
border-radius: var(--radius-md);
font-weight: 700;
text-align: center;
min-width: 150px;
border: 2px solid transparent;
box-shadow: var(--shadow-sm);
}
.state-pending {
background: #fef3c7;
color: #92400e;
border-color: #f59e0b;
}
.state-fulfilled {
background: #dcfce7;
color: #166534;
border-color: #22c55e;
}
.state-rejected {
background: #ffe4e6;
color: #9f1239;
border-color: #f43f5e;
}
.diagram-arrow {
font-size: 1.5rem;
color: var(--text-muted);
font-weight: 800;
}
.analogy-box {
background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
border-left: 4px solid var(--primary);
border-radius: var(--radius-sm);
padding: 18px 20px;
margin: 18px 0;
}
</style>
</head>
<body>
<div class="app-layout">
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🌐 JavaScript Asynchronität</div>
<h1>JS 05: Callbacks, Event Loop &amp; Promises</h1>
<p>Meistere nicht-blockierenden Code, den Event Loop und moderne Promises für robuste Web-Anwendungen.</p>
</div>
</header>
<main class="container">
<!-- Abschnitt 1: Konzept & Event Loop -->
<section class="card">
<h2>⚡ 1. Synchron vs. Asynchron &amp; Event Loop</h2>
<p>
JavaScript ist <strong>Single-Threaded</strong>. Wenn eine Aufgabe Zeit benötigt (z.&nbsp;B. Timer, Netzwerk-Aufrufe, Dateilesen), darf sie den Hauptthread nicht blockieren.
</p>
<p>
Der <strong>Event Loop</strong> sorgt dafür, dass asynchrone Callbacks in einer Warteschlange verwaltet und erst ausgeführt werden, wenn der Haupt-Callstack frei ist.
</p>
<div class="analogy-box">
<h3>🍔 Die Restaurant-Pieper-Analogie</h3>
<p>
Bestellst du in einem Restaurant dein Essen, musst du <em>nicht</em> starr an der Kasse stehen bleiben. Du erhältst sofort einen <strong>Bestell-Pieper (ein Promise im Zustand <code>pending</code>)</strong>. Du setzt dich entspannt an deinen Tisch (der Event Loop verarbeitet andere Dinge).
</p>
<p style="margin-bottom: 0;">
Ist dein Menü fertig, vibriert der Pieper (<strong><code>resolve</code> &rarr; <code>fulfilled</code></strong>). Brennt die Küche, blinkt er rot (<strong><code>reject</code> &rarr; <code>rejected</code></strong>).
</p>
</div>
</section>
<!-- Abschnitt 2: Was ist ein Promise & Zustandsdiagramm -->
<section class="card">
<h2>📊 2. Das Promise-Zustandsdiagramm</h2>
<p>
Ein <code>Promise</code> ist ein Stellvertreter-Objekt für einen zukünftigen Wert. Es befindet sich immer in genau einem von drei Zuständen:
</p>
<div class="promise-diagram">
<div class="state-box state-pending">
⏳ pending<br>
<small>(wartend / schwebend)</small>
</div>
<div style="display: flex; flex-direction: column; gap: 14px; align-items: center;">
<div style="display: flex; align-items: center; gap: 8px;">
<span class="diagram-arrow">&rarr;</span>
<span style="font-size: 0.85rem; font-weight: 700; color: #166534;">resolve(wert)</span>
<span class="diagram-arrow">&rarr;</span>
<div class="state-box state-fulfilled">
✅ fulfilled<br>
<small>.then(wert => ...)</small>
</div>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="diagram-arrow">&rarr;</span>
<span style="font-size: 0.85rem; font-weight: 700; color: #9f1239;">reject(error)</span>
<span class="diagram-arrow">&rarr;</span>
<div class="state-box state-rejected">
❌ rejected<br>
<small>.catch(err => ...)</small>
</div>
</div>
</div>
</div>
</section>
<!-- Abschnitt 3: Code-Muster & Chaining -->
<section class="card">
<h2>🛠️ 3. Eigene Promises &amp; Chaining</h2>
<p>Ein Promise wird mit <code>new Promise((resolve, reject) => { ... })</code> erzeugt:</p>
<pre><code>// 1. Promise definieren
function ladeDaten(id) {
return new Promise((resolve, reject) => {
if (id > 0) {
setTimeout(() => resolve({ id: id, status: "ok" }), 100);
} else {
reject(new Error("ID muss positiv sein"));
}
});
}
// 2. Promise verketten (Chaining)
ladeDaten(42)
.then(daten => {
console.log("Geladen:", daten);
return daten.id * 2;
})
.then(verdoppelt => console.log("Verdoppelt:", verdoppelt))
.catch(err => console.error("Fehler:", err.message))
.finally(() => console.log("Vorgang abgeschlossen."));</code></pre>
<h3 style="margin-top: 24px;">🚀 Parallelisierung mit <code>Promise.all()</code></h3>
<pre><code>// Lädt quelleA und quelleB parallel und wartet auf beide
Promise.all([quelleA, quelleB])
.then(([resA, resB]) => console.log("Beide fertig:", resA, resB))
.catch(err => console.error("Mindestens eine Quelle schlug fehl:", err));</code></pre>
</section>
<!-- Abschnitt 4: Aufgaben & Checkliste -->
<section class="card">
<h2>🎯 Aufgaben &amp; Checkliste</h2>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t1">
<label class="task-label" for="t1">
<strong>TODO 1:</strong> <code>verzoegereAusfuehrung(ms, rueckgabeWert)</code> mit <code>setTimeout</code> &amp; <code>resolve</code> bauen.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t2">
<label class="task-label" for="t2">
<strong>TODO 2:</strong> <code>pruefeZahlGroesserNull(zahl)</code> mit <code>resolve</code> bei &gt; 0 bzw. <code>reject</code> bei &lt;= 0 schreiben.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t3">
<label class="task-label" for="t3">
<strong>TODO 3:</strong> <code>ladeBenutzerProfil(userId)</code> simulieren und Profil-Objekt auflösen.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t4">
<label class="task-label" for="t4">
<strong>TODO 4:</strong> <code>ladeAlleDatenQuellen(quelleA, quelleB)</code> mittels <code>Promise.all()</code> parallel aggregieren.
</label>
</div>
<!-- Lösungstipps Collapsible -->
<details style="margin-top: 20px; padding: 14px; background: #f8fafc; border: 1px solid var(--border-color); border-radius: var(--radius-sm);">
<summary style="cursor: pointer; font-weight: 700; color: var(--primary);">💡 Lösungstipps &amp; Hilfestellung</summary>
<ul style="margin-top: 10px; margin-left: 20px; font-size: 0.92rem; color: var(--text-body);">
<li>Nutze <code>return new Promise((resolve, reject) => { ... });</code>.</li>
<li>Innerhalb des Executors rufst du <code>resolve(wert)</code> bei Erfolg oder <code>reject(new Error("..."))</code> bei Fehlern auf.</li>
<li><code>Promise.all</code> nimmt ein Array entgegen: <code>Promise.all([quelleA, quelleB])</code>.</li>
</ul>
</details>
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="../../../workspace.html?course=javascript&track=track_2_async_und_apis&chapter=05_callbacks_und_promises" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen &rarr;</a>
<a href="../06_async_await_und_fetch/index.html" class="btn" style="background: #4f46e5; color: #fff; font-weight: 600; text-decoration: none; padding: 10px 20px; border-radius: 6px;">Weiter zu Modul 06 &rarr;</a>
</div>
</section>
</main>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,42 @@
/**
* 🌐 JS 05: MUSTERLÖSUNG – CALLBACKS & PROMISES 🌐
* ===============================================
*/
function verzoegereAusfuehrung(ms, rueckgabeWert) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(rueckgabeWert);
}, ms);
});
}
function pruefeZahlGroesserNull(zahl) {
return new Promise((resolve, reject) => {
if (typeof zahl === "number" && zahl > 0) {
resolve(zahl);
} else {
reject(new Error("Zahl muss größer als 0 sein"));
}
});
}
function ladeBenutzerProfil(userId) {
return new Promise((resolve, reject) => {
if (typeof userId === "number" && userId > 0) {
setTimeout(() => {
resolve({
id: userId,
name: `Benutzer_${userId}`,
status: "aktiv"
});
}, 10);
} else {
reject(new Error("Ungültige User-ID"));
}
});
}
function ladeAlleDatenQuellen(quelleA, quelleB) {
return Promise.all([quelleA, quelleB]);
}

View file

@ -0,0 +1,78 @@
// Hilfsfunktion zur Rejection-Prüfung
async function pruefeReject(promise, expectedMsg, beschreibung) {
let threw = false;
let caughtErr = null;
try {
await promise;
} catch (err) {
threw = true;
caughtErr = err;
if (expectedMsg && !String(err && (err.message || err)).includes(expectedMsg)) {
throw new Error(`${beschreibung}: Erwartete Fehlermeldung "${expectedMsg}", aber erhalten: "${err ? err.message : err}"`);
}
}
if (!threw) {
throw new Error(`${beschreibung}: Promise sollte abgewiesen werden (reject), wurde aber erfolgreich aufgelöst.`);
}
}
// 1. Test: verzoegereAusfuehrung
const erg1 = await verzoegereAusfuehrung(20, "Erfolg 20ms");
assert.strictEqual(erg1, "Erfolg 20ms", "verzoegereAusfuehrung liefert falschen Wert nach Ablauf des Timers");
// 2. Test: pruefeZahlGroesserNull bei positiver Zahl
const erg2 = await pruefeZahlGroesserNull(42);
assert.strictEqual(erg2, 42, "pruefeZahlGroesserNull(42) sollte mit 42 resolven");
// 3. Test: pruefeZahlGroesserNull bei 0 und negativer Zahl
await pruefeReject(
pruefeZahlGroesserNull(-5),
"Zahl muss größer als 0 sein",
"pruefeZahlGroesserNull(-5)"
);
await pruefeReject(
pruefeZahlGroesserNull(0),
"Zahl muss größer als 0 sein",
"pruefeZahlGroesserNull(0)"
);
// 4. Test: ladeBenutzerProfil mit gültiger ID
const profil = await ladeBenutzerProfil(101);
assert.deepStrictEqual(
profil,
{ id: 101, name: "Benutzer_101", status: "aktiv" },
"ladeBenutzerProfil(101) liefert falsches Profil-Objekt"
);
// 5. Test: ladeBenutzerProfil mit ungültiger ID
await pruefeReject(
ladeBenutzerProfil(0),
"Ungültige User-ID",
"ladeBenutzerProfil(0)"
);
await pruefeReject(
ladeBenutzerProfil(-10),
"Ungültige User-ID",
"ladeBenutzerProfil(-10)"
);
// 6. Test: ladeAlleDatenQuellen mit zwei erfolgreichen Promises (Promise.all)
const pA = verzoegereAusfuehrung(15, "Sensordaten Alpha");
const pB = verzoegereAusfuehrung(10, "Sensordaten Beta");
const alleQuellen = await ladeAlleDatenQuellen(pA, pB);
assert.deepStrictEqual(
alleQuellen,
["Sensordaten Alpha", "Sensordaten Beta"],
"ladeAlleDatenQuellen sollte alle Ergebnisse im Array zusammenfassen"
);
// 7. Test: ladeAlleDatenQuellen mit einem abgewiesenen Promise
const pGut = verzoegereAusfuehrung(10, "Gute Daten");
const pSchlecht = pruefeZahlGroesserNull(-100);
await pruefeReject(
ladeAlleDatenQuellen(pGut, pSchlecht),
"Zahl muss größer als 0 sein",
"ladeAlleDatenQuellen mit abgewiesenem Promise"
);
console.log("✅ Alle 7 Asynchronitäts- und Promise-Tests in JS 05 erfolgreich bestanden!");

View file

@ -0,0 +1,142 @@
# 🌐 JS 06: Async/Await & REST APIs (Fetch)
Willkommen zu **Modul 06** des JavaScript-Kurses! Hier lernst du die modernste und eleganteste Art kennen, um mit asynchronen Daten und Web-APIs in JavaScript zu arbeiten: **`async` und `await`**.
---
## 📖 1. Was ist `async` / `await`?
Mit ECMAScript 2017 (ES8) wurde `async/await` eingeführt. Es ist sogenannte *syntaktische Versüßung* (Syntactic Sugar) über JavaScript Promises:
- Asynchroner Code sieht plötzlich aus und verhält sich wie **linearer, synchroner Code**.
- Keine unübersichtlichen `.then()`-Ketten mehr.
- Fehler werden mit gewohntem `try { ... } catch (err) { ... }` abgefangen.
---
## 📚 2. Die Buch-Lese-Analogie
Stell dir vor, wie du ein Buch liest:
- **Mit Callbacks & Chaining (`.then()`):** Es ist wie ein Buch, bei dem jedes Kapitel auf Seite 80 verweist (*„Wenn du wissen willst, wie es weitergeht, lies Fußnote 4 auf Seite 80 und springe dann zu Kapitel 7“*). Du springst ständig hin und her.
- **Mit `async/await`:** Du liest die Geschichte ganz natürlich **Zeile für Zeile von oben nach unten**. Wenn an einer Stelle ein Briefbote eintrifft (asynchrones Laden via `await`), pausiert der Erzähler kurz, nimmt den Brief entgegen und liest nahtlos im Text weiter.
---
## ⚙️ 3. Die `async`-Funktion
Wird das Schlüsselwort `async` vor eine Funktion gesetzt:
1. Gibt die Funktion **immer** ein `Promise` zurück.
2. Jeder `return`-Wert wird automatisch in ein aufgelöstes Promise (`Promise.resolve(...)`) eingepackt.
3. Jeder nicht abgefangene Fehler (`throw new Error(...)`) wird zu einem abgewiesenen Promise (`Promise.reject(...)`).
```javascript
async function holeBegruessung(name) {
return `Hallo ${name}!`; // Gibt Promise { <fulfilled>: "Hallo Anna!" } zurück
}
holeBegruessung("Anna").then(text => console.log(text));
```
---
## ⏳ 4. Der `await`-Operator
`await` kann nur innerhalb von `async`-Funktionen verwendet werden. Es pausiert die Ausführung der aktuellen Funktion, bis das Promise aufgelöst oder abgewiesen wurde:
```javascript
async function ladeBenutzer() {
console.log("Starte Abruf...");
// Pausiert die Funktion 'ladeBenutzer', aber blockiert NICHT den Browser-Thread!
const benutzer = await apiCall("/user/1");
console.log("Fertig:", benutzer.name);
return benutzer;
}
```
---
## ⚖️ 5. Direktvergleich: Promises vs. Async/Await
### Vorher: Promise-Chaining (`.then()`)
```javascript
function holeDatenMitThen() {
return fetch("https://api.beispiel.de/daten")
.then(response => {
if (!response.ok) throw new Error("Netzwerkfehler");
return response.json();
})
.then(daten => {
console.log("Ergebnis:", daten);
return daten;
})
.catch(err => {
console.error("Fehler:", err.message);
});
}
```
### Jetzt: Modernes `async/await`
```javascript
async function holeDatenMitAsync() {
try {
const response = await fetch("https://api.beispiel.de/daten");
if (!response.ok) throw new Error(`HTTP-Fehler: ${response.status}`);
const daten = await response.json();
console.log("Ergebnis:", daten);
return daten;
} catch (err) {
console.error("Fehler:", err.message);
}
}
```
---
## 🌐 6. REST-APIs abfragen mit `fetch()`
Beim Abfragen einer REST-API mit `fetch()` laufen in der Praxis zwei asynchrone Schritte ab:
1. **HTTP-Verbindung aufbauen & Statuscode prüfen:**
```javascript
const response = await fetch(url);
if (!response.ok) {
// response.status enthält den HTTP-Code (z. B. 404 Not Found oder 500 Server Error)
throw new Error(`Fehler: ${response.status}`);
}
```
2. **JSON-Body asynchron parsen:**
```javascript
const jsonData = await response.json();
```
---
## 🛡️ 7. Robuste Fehlerbehandlung mit `try ... catch`
Mit `try ... catch` können sowohl synchrone JavaScript-Laufzeitfehler als auch asynchrone Promise-Rejections an zentraler Stelle abgefangen werden:
```javascript
async function sichereAktion() {
try {
const erg = await riskanteOperation();
return erg;
} catch (err) {
console.warn("Operation fehlgeschlagen, verwende Fallback:", err.message);
return { status: "fallback_aktiv" };
}
}
```
---
## 🎯 8. Deine Aufgaben in `aufgabe.js`
1. **`holeWetterDaten(stadtName, mockApiFetcher)`**:
Rufe `await mockApiFetcher(stadtName)` auf. Prüfe `response.ok`. Wenn `false`, wirf einen Fehler. Wenn `true`, parse JSON (`await response.json()`) und gib `{ stadt, temperatur, wetterlage }` zurück.
2. **`aggregiereBenutzerPosts(userId, mockPostApiFetcher)`**:
Rufe `await mockPostApiFetcher(userId)` auf. Aggregiere die Posts zu `{ userId, anzahlPosts, postTitel }`.
3. **`sichererApiAufruf(apiPromiseOderFn, standardFallback)`**:
Führe ein Promise oder eine async Funktion im `try`-Block aus. Fange Fehler im `catch`-Block ab und gib `standardFallback` zurück.

View file

@ -0,0 +1,42 @@
/**
* 🌐 JS 06: ASYNC / AWAIT & REST-APIS (FETCH) 🌐
* ===============================================
* Lerne modernes asynchrones JavaScript mit async/await,
* robuster try-catch-Fehlerbehandlung und dem Konsumieren von REST-APIs.
*/
// 🎯 TEILZIEL 1 (TODO 1): Schreibe holeWetterDaten(stadtName, mockApiFetcher)
// - Ruft mockApiFetcher(stadtName) mit await auf.
// - Das Response-Objekt enthält { ok: boolean, status: number, json: async () => data }.
// - Wenn response.ok === false ist: wirf einen Fehler (throw new Error(`Stadt ${stadtName} nicht gefunden`)).
// - Wenn response.ok === true ist: parse das JSON mit await response.json()
// - Gib ein formatiertes Objekt zurück:
// { stadt: data.stadt, temperatur: data.temperatur, wetterlage: data.wetterlage }
async function holeWetterDaten(stadtName, mockApiFetcher) {
// TODO: Nutze await, prüfe response.ok, parse JSON und gib das formatierte Objekt zurück
return null;
}
// 🎯 TEILZIEL 2 (TODO 2): Schreibe aggregiereBenutzerPosts(userId, mockPostApiFetcher)
// - Ruft mockPostApiFetcher(userId) mit await auf.
// - Wenn der Fetcher eine Response liefert (response.ok), parse das JSON; wenn direkt ein Array geliefert wird, nutze dieses.
// - Extrahiere alle Posts des Nutzers.
// - Gib ein Aggregat-Objekt zurück mit folgendem Aufbau:
// {
// userId: userId,
// anzahlPosts: posts.length,
// postTitel: posts.map(p => p.titel)
// }
async function aggregiereBenutzerPosts(userId, mockPostApiFetcher) {
// TODO: Asynchron abrufen und aggregieren
return null;
}
// 🎯 TEILZIEL 3 (TODO 3): Schreibe sichererApiAufruf(apiPromiseOderFn, standardFallback)
// - Führt das übergebene Promise oder die asynchrone Funktion in einem try-Block mit await aus.
// - Wenn die Ausführung erfolgreich ist: gib das Ergebnis zurück.
// - Wenn ein Fehler auftritt (catch): fange den Fehler ab und gib standardFallback zurück.
async function sichererApiAufruf(apiPromiseOderFn, standardFallback) {
// TODO: try-catch Block für sicheren Aufruf mit Fallback-Wert
return null;
}

View file

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS 06: Async/Await &amp; REST-APIs (Fetch)</title>
<link rel="stylesheet" href="../../../assets/style.css">
<style>
.comparison-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
margin: 20px 0;
}
.comparison-col {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 16px;
}
.comparison-col h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.05rem;
}
.analogy-box {
background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
border-left: 4px solid var(--primary);
border-radius: var(--radius-sm);
padding: 18px 20px;
margin: 18px 0;
}
.api-step {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
padding: 10px 14px;
background: var(--bg-subtle);
border-radius: var(--radius-sm);
}
.api-step-badge {
background: var(--primary);
color: white;
font-weight: 800;
font-size: 0.8rem;
padding: 2px 8px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
</style>
</head>
<body>
<div class="app-layout">
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🌐 Modernes JavaScript</div>
<h1>JS 06: Async/Await &amp; REST-APIs (Fetch)</h1>
<p>Schreibe asynchronen Code so lesbar und linear wie synchronen Code mit <code>async</code>, <code>await</code> und <code>try/catch</code>.</p>
</div>
</header>
<main class="container">
<!-- Abschnitt 1: Konzept & Buch-Analogie -->
<section class="card">
<h2>📖 1. Warum Async/Await?</h2>
<p>
Mit <code>async/await</code> (ES2017) gehört unübersichtliches Promise-Chaining der Vergangenheit an. Asynchroner Code liest sich nun von oben nach unten wie ganz normaler, synchroner Code.
</p>
<div class="analogy-box">
<h3>📚 Die Buch-Lese-Analogie</h3>
<p>
<strong>Vorher (Callbacks &amp; .then()):</strong> Wie ein Buch, bei dem jedes Kapitel auf Fußnoten verweist (<em>„Lies auf Seite 80 weiter, um die Antwort zu finden“</em>).
</p>
<p style="margin-bottom: 0;">
<strong>Jetzt (Async/Await):</strong> Du liest das Buch ganz entspannt <strong>Zeile für Zeile von oben nach unten</strong>. Bei <code>await</code> pausiert der Erzähler kurz, holt die Information ab und liest genau an der Stelle flüssig weiter!
</p>
</div>
</section>
<!-- Abschnitt 2: Direktvergleich -->
<section class="card">
<h2>⚖️ 2. Gegenüberstellung: Promises vs. Async/Await</h2>
<div class="comparison-grid">
<div class="comparison-col">
<h3 style="color: #64748b;">⏳ Früher: Promise .then() Chaining</h3>
<pre><code>function ladeProfil(id) {
fetch(`/api/user/${id}`)
.then(res => {
if (!res.ok) throw new Error("404");
return res.json();
})
.then(user => {
console.log("Name:", user.name);
})
.catch(err => {
console.error("Fehler:", err);
});
}</code></pre>
</div>
<div class="comparison-col" style="border-color: var(--primary);">
<h3 style="color: var(--primary);">✨ Heute: Modernes Async/Await</h3>
<pre><code>async function ladeProfil(id) {
try {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) throw new Error("404");
const user = await res.json();
console.log("Name:", user.name);
} catch (err) {
console.error("Fehler:", err);
}
}</code></pre>
</div>
</div>
</section>
<!-- Abschnitt 3: REST-APIs & Fetch-Muster -->
<section class="card">
<h2>🌐 3. REST-APIs konsumieren</h2>
<p>Typischer 2-Schritt-Ablauf beim Abrufen einer JSON-REST-API:</p>
<div class="api-step">
<span class="api-step-badge">Schritt 1</span>
<div>
<strong>HTTP-Request senden &amp; Statuscode prüfen:</strong>
<p style="margin: 4px 0 0 0; font-size: 0.9rem;">
<code>const res = await fetch(url);</code><br>
<code>if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`);</code>
</p>
</div>
</div>
<div class="api-step">
<span class="api-step-badge">Schritt 2</span>
<div>
<strong>JSON-Body asynchron auslesen:</strong>
<p style="margin: 4px 0 0 0; font-size: 0.9rem;">
<code>const daten = await res.json();</code>
</p>
</div>
</div>
</section>
<!-- Abschnitt 4: Aufgaben & Checkliste -->
<section class="card">
<h2>🎯 Aufgaben &amp; Checkliste (Subgoals)</h2>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t1">
<label class="task-label" for="t1">
<strong>TODO 1:</strong> <code>holeWetterDaten(stadtName, mockApiFetcher)</code> mit <code>await</code>, <code>res.ok</code>-Prüfung und <code>res.json()</code> implementieren.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t2">
<label class="task-label" for="t2">
<strong>TODO 2:</strong> <code>aggregiereBenutzerPosts(userId, mockPostApiFetcher)</code> schreiben und Beitragsdaten aggregieren.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t3">
<label class="task-label" for="t3">
<strong>TODO 3:</strong> <code>sichererApiAufruf(apiPromiseOderFn, standardFallback)</code> mit <code>try { ... } catch</code> absichern.
</label>
</div>
<!-- Lösungstipps Collapsible -->
<details style="margin-top: 20px; padding: 14px; background: #f8fafc; border: 1px solid var(--border-color); border-radius: var(--radius-sm);">
<summary style="cursor: pointer; font-weight: 700; color: var(--primary);">💡 Lösungstipps &amp; Hilfestellung</summary>
<ul style="margin-top: 10px; margin-left: 20px; font-size: 0.92rem; color: var(--text-body);">
<li>Deklariere jede asynchrone Funktion mit <code>async function ...</code>.</li>
<li>Prüfe vor <code>await res.json()</code> immer mit <code>if (!res.ok)</code> auf HTTP-Fehler.</li>
<li>Im <code>try / catch</code>-Block fängst du fehlgeschlagene Promises sauber ab und lieferst den Fallback-Wert.</li>
</ul>
</details>
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="../05_callbacks_und_promises/index.html" class="btn" style="background: #64748b; color: #fff; font-weight: 600; text-decoration: none; padding: 10px 20px; border-radius: 6px;">&larr; Zurück zu Modul 05</a>
<a href="../../../workspace.html?course=javascript&track=track_2_async_und_apis&chapter=06_async_await_und_fetch" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen &rarr;</a>
</div>
</section>
</main>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,50 @@
/**
* 🌐 JS 06: MUSTERLÖSUNG – ASYNC / AWAIT & REST-APIS 🌐
* ====================================================
*/
async function holeWetterDaten(stadtName, mockApiFetcher) {
const response = await mockApiFetcher(stadtName);
if (!response || !response.ok) {
const status = response ? response.status : 500;
throw new Error(`Stadt ${stadtName} nicht gefunden (Status: ${status})`);
}
const data = await response.json();
return {
stadt: data.stadt,
temperatur: data.temperatur,
wetterlage: data.wetterlage
};
}
async function aggregiereBenutzerPosts(userId, mockPostApiFetcher) {
const response = await mockPostApiFetcher(userId);
let posts;
if (response && typeof response.json === "function") {
if (!response.ok) {
throw new Error(`Fehler beim Laden der Posts für User ${userId}`);
}
posts = await response.json();
} else if (Array.isArray(response)) {
posts = response;
} else {
posts = [];
}
return {
userId: userId,
anzahlPosts: posts.length,
postTitel: posts.map((p) => p.titel)
};
}
async function sichererApiAufruf(apiPromiseOderFn, standardFallback) {
try {
if (typeof apiPromiseOderFn === "function") {
return await apiPromiseOderFn();
}
return await apiPromiseOderFn;
} catch (err) {
return standardFallback;
}
}

View file

@ -0,0 +1,138 @@
// Hilfsfunktion für Fehler-Prüfung
async function pruefeReject(promise, expectedMsg, beschreibung) {
let threw = false;
let caughtErr = null;
try {
await promise;
} catch (err) {
threw = true;
caughtErr = err;
if (expectedMsg && !String(err && (err.message || err)).includes(expectedMsg)) {
throw new Error(`${beschreibung}: Erwartete Fehlermeldung "${expectedMsg}", aber erhalten: "${err ? err.message : err}"`);
}
}
if (!threw) {
throw new Error(`${beschreibung}: Funktion sollte Fehler werfen / rejecten, war aber erfolgreich.`);
}
}
// ----------------------------------------------------
// Mock-APIs definieren
// ----------------------------------------------------
const mockWetterApi = async (stadt) => {
if (stadt === "Berlin") {
return {
ok: true,
status: 200,
json: async () => ({ stadt: "Berlin", temperatur: 21, wetterlage: "Sonnig", feuchtigkeit: "55%" })
};
} else if (stadt === "München") {
return {
ok: true,
status: 200,
json: async () => ({ stadt: "München", temperatur: 18, wetterlage: "Regnerisch" })
};
} else {
return {
ok: false,
status: 404,
json: async () => ({ fehler: "Stadt nicht gefunden" })
};
}
};
const mockPostApi = async (userId) => {
if (userId === 10) {
return {
ok: true,
status: 200,
json: async () => [
{ id: 1, titel: "Einführung in JS" },
{ id: 2, titel: "Async Await Guide" },
{ id: 3, titel: "REST-APIs verstehen" }
]
};
} else if (userId === 99) {
return [
{ id: 4, titel: "Einzelbeitrag" }
];
} else {
return {
ok: true,
status: 200,
json: async () => []
};
}
};
// ----------------------------------------------------
// 1. Test: holeWetterDaten (Erfolg)
// ----------------------------------------------------
const wetterBerlin = await holeWetterDaten("Berlin", mockWetterApi);
assert.deepStrictEqual(
wetterBerlin,
{ stadt: "Berlin", temperatur: 21, wetterlage: "Sonnig" },
"holeWetterDaten('Berlin') liefert unerwartetes Wetter-Objekt"
);
const wetterMuenchen = await holeWetterDaten("München", mockWetterApi);
assert.strictEqual(wetterMuenchen.temperatur, 18, "Temperatur für München sollte 18 sein");
assert.strictEqual(wetterMuenchen.wetterlage, "Regnerisch", "Wetterlage für München sollte Regnerisch sein");
// ----------------------------------------------------
// 2. Test: holeWetterDaten (404 Fehler)
// ----------------------------------------------------
await pruefeReject(
holeWetterDaten("Atlantis", mockWetterApi),
"Atlantis",
"holeWetterDaten für unbekannte Stadt muss Fehler werfen"
);
// ----------------------------------------------------
// 3. Test: aggregiereBenutzerPosts
// ----------------------------------------------------
const posts10 = await aggregiereBenutzerPosts(10, mockPostApi);
assert.deepStrictEqual(
posts10,
{
userId: 10,
anzahlPosts: 3,
postTitel: ["Einführung in JS", "Async Await Guide", "REST-APIs verstehen"]
},
"aggregiereBenutzerPosts(10) liefert falsches Aggregat"
);
const posts99 = await aggregiereBenutzerPosts(99, mockPostApi);
assert.strictEqual(posts99.anzahlPosts, 1, "User 99 sollte genau 1 Post haben");
assert.deepStrictEqual(posts99.postTitel, ["Einzelbeitrag"], "Post-Titel für User 99 fehlerhaft");
const postsLeer = await aggregiereBenutzerPosts(999, mockPostApi);
assert.strictEqual(postsLeer.anzahlPosts, 0, "User 999 sollte 0 Posts haben");
assert.deepStrictEqual(postsLeer.postTitel, [], "Leere Post-Titel-Liste erwartet");
// ----------------------------------------------------
// 4. Test: sichererApiAufruf
// ----------------------------------------------------
// 4a: Erfolgreiches Promise
const ergErfolg = await sichererApiAufruf(Promise.resolve("Daten geladen!"), "Standard");
assert.strictEqual(ergErfolg, "Daten geladen!", "sichererApiAufruf sollte den aufgelösten Wert zurückgeben");
// 4b: Fehlgeschlagenes Promise (muss abgefangen werden)
const ergFehler = await sichererApiAufruf(
Promise.reject(new Error("Netzwerk-Timeout")),
"Standard-Fallback"
);
assert.strictEqual(ergFehler, "Standard-Fallback", "sichererApiAufruf sollte Fallback zurückgeben bei Rejection");
// 4c: Async Funktion, die eine Exception wirft
const ergFnFehler = await sichererApiAufruf(
async () => { throw new Error("Kritischer API Crash"); },
{ status: "offline", data: [] }
);
assert.deepStrictEqual(
ergFnFehler,
{ status: "offline", data: [] },
"sichererApiAufruf sollte Fallback-Objekt bei geworfenem Fehler liefern"
);
console.log("✅ Alle Tests in JS 06 (Async/Await & REST-APIs) erfolgreich bestanden!");

View file

@ -0,0 +1,151 @@
# 🛡️ Modul 07: TypeScript Type Safety & Interfaces
> **Leitgedanke:** *„TypeScript ist wie ein digitaler Sicherheitsgurt und ein unbestechlicher Bauplan-Wächter: Fehler werden bereits während der Entwicklung im Editor erkannt – und nicht erst nachts beim Kunden auf der Website!“*
---
## 📚 Übersicht & Lernziele
In diesem Modul machst du den entscheidenden Schritt von dynamischem JavaScript hin zur **statischen Typsicherheit mit TypeScript**:
1. **Warum TypeScript?** – Verstehe den Unterschied zwischen Laufzeit- und Compile-Time-Fehlern.
2. **Basis-Datentypen** – `string`, `number`, `boolean`, `string[]`, `any`, `unknown`, `void`.
3. **Type Aliases & Union Types** – Flexible Kombinationen wie `type ID = string | number;`.
4. **Interfaces & Optionale Eigenschaften** – Strukturverträge mit `interface` und `?`.
5. **Generics (`<T>`)** – Wiederverwendbare, typsichere Algorithmen und Hilfsfunktionen.
6. **Optional Chaining (`?.`) & Nullish Coalescing (`??`)** – Sichere Zugriffe ohne `Cannot read properties of undefined`.
---
## 💡 1. Die didaktische Analogie: Der Bauplan-Wächter
| Klassisches JavaScript ⚡ | TypeScript mit Interfaces 🛡️ |
| :--- | :--- |
| **Baustelle ohne Plan:** Man baut drauflos. Ob ein Stein passt oder eine tragende Wand fehlt, merkt man erst, wenn das Haus einstürzt (Laufzeit-Crash beim Kunden). | **Architekten-Bauplan mit TÜV-Prüfung:** Jede Schnittstelle, jeder Balken und jedes Kabel ist exakt typisiert. Passt ein Element nicht, schlägt der Compiler sofort Alarm. |
---
## 🔍 2. TypeScript Kernkonzepte im Detail
### A. Basis-Datentypen
```typescript
// Explizite Typ-Annotationen
const benutzername: string = "Anna_Dev";
const alter: number = 28;
const istAdmin: boolean = true;
// Arrays
const tags: string[] = ["typescript", "frontend", "web"];
const noten: Array<number> = [1, 2, 1, 3];
// Spezialtypen: any vs. unknown vs. void
let beliebig: any = "Kann alles sein (Typ-Check deaktiviert)";
let unsicher: unknown = JSON.parse('{"status": "ok"}'); // Erfordert Typprüfung vor Nutzung!
function loggeNachricht(msg: string): void {
console.log(`[LOG]: ${msg}`);
// Kein Return-Wert -> void
}
```
### B. Type Aliases & Union Types
Mit Union Types (`|`) können Variablen mehrere definierte Typen annehmen:
```typescript
// Type Alias für eindeutige Identifikatoren
type ID = string | number;
// Literal-Union-Type für feste Status-Werte
type AuftragsStatus = "offen" | "in_bearbeitung" | "abgeschlossen" | "storniert";
let meineId: ID = 42;
meineId = "USR-9921"; // ✅ Beides absolut gültig!
```
### C. Interfaces (Objekt-Verträge)
Ein Interface definiert die exakte Form eines JavaScript-Objekts. Mit dem Fragezeichen `?` werden Felder als optional markiert:
```typescript
interface User {
id: ID;
name: string;
istAktiv: boolean;
email?: string; // ❓ Optional: Kann vorhanden sein oder undefined
erstelltAm: string;
}
const kunde1: User = {
id: 101,
name: "Max Mustermann",
istAktiv: true,
erstelltAm: "2026-08-30T00:00:00.000Z"
};
```
### D. Generics (`<T>` – Typ-Parameter)
Generics erlauben es, Funktionen und Klassen zu schreiben, die mit beliebigen Datentypen arbeiten, dabei aber die volle Typsicherheit behalten:
```typescript
// Generische Umkehrfunktion für Arrays
function umkehren<T>(liste: T[]): T[] {
return [...liste].reverse();
}
const zahlenUmgekehrt = umkehren<number>([1, 2, 3, 4]); // Typ: number[]
const worteUmgekehrt = umkehren<string>(["A", "B", "C"]); // Typ: string[]
```
### E. Optional Chaining (`?.`) & Nullish Coalescing (`??`)
```typescript
interface Profil {
avatar?: {
url?: string;
};
}
const user: Profil = {};
// Sicherer Zugriff ohne Absturz:
const avatarUrl = user?.avatar?.url ?? "default_avatar.png";
console.log(avatarUrl); // -> "default_avatar.png"
```
---
## 🎯 3. Deine Aufgaben in `aufgabe.js`
Implementiere die folgenden 4 Kernfunktionen:
### 🎯 TODO 1: `erstelleKunde(id, name, istAktiv = true, email = null)`
- Erzeugt ein valides Kunden-Objekt `{ id, name, istAktiv, [email], erstelltAm }`.
- Wirft einen `TypeError`, wenn `id` weder `string` noch `number` ist, oder wenn `name` leer ist.
- Säubert den Namen mit `.trim()`.
### 🎯 TODO 2: `validiereAuftrag(auftragObjekt)`
- Validiert ein übergebenes Auftrag-Objekt gegen das `Auftrag`-Interface.
- Prüft `id`, `kundeId`, `positionen` (Array mit mind. 1 Element, validen Mengen > 0 & Einzelpreisen >= 0), `summe` (>= 0) und `status` ('offen', 'in_bearbeitung', 'abgeschlossen', 'storniert').
- Liefert `{ gueltig: boolean, fehler: string[] }`.
### 🎯 TODO 3: `filtereNachStatus(eintraege, zielStatus)`
- Generische Hilfsfunktion: Filtert ein Array von Objekten nach `item.status === zielStatus`.
- Liefert bei ungültigen Eingaben sicher ein leeres Array `[]`.
### 🎯 TODO 4: `formatierePreisangabe(betrag, waehrung = "EUR")`
- Formatiert Zahlen typsicher im deutschen Währungsformat (z.B. `49.99` -> `"49,99 EUR"`).
- Wirft bei ungültigen Beträgen (z.B. Strings, `NaN`) oder leeren Währungen einen `TypeError`.
---
## 🧪 4. Selbstüberprüfung
Führe die Unittests in der Web-IDE oder im Test-Runner aus:
- ✅ 1. Kunden-Erstellung mit Standardwerten & Fehlervalidierung.
- ✅ 2. Vollständige Auftragsvalidierung mit Fehler-Reporting.
- ✅ 3. Generischer Statusfilter.
- ✅ 4. Typsichere Preis- und Währungsformatierung.

View file

@ -0,0 +1,122 @@
/**
* 🌐 JS/TS 07: TYPESCRIPT TYPE SAFETY & INTERFACES 🌐
* ====================================================
* In diesem Modul lernst du, wie statische Typsicherheit,
* Interfaces, Type Aliases und Generics deine Software vor
* Laufzeitfehlern schützen ("Digitaler Sicherheitsgurt").
*/
/**
* @typedef {Object} Kunde
* @property {string | number} id - Eindeutige Kunden-ID
* @property {string} name - Vollständiger Name des Kunden
* @property {boolean} istAktiv - Status des Kunden
* @property {string} [email] - Optionale E-Mail-Adresse
* @property {string} erstelltAm - ISO-Datumsstring der Erstellung
*/
/**
* 🎯 TEILZIEL 1 (TODO 1): erstelleKunde(id, name, istAktiv = true, email = null)
* Erstellt ein typsicheres Kundenobjekt.
*
* Anforderungen:
* - id: string | number (nicht leer)
* - name: string (darf nach .trim() nicht leer sein)
* - istAktiv: boolean (Standard: true)
* - email: string | null (optional)
* - erstelltAm: aktueller ISO-Datumsstring (z.B. new Date().toISOString())
* - Wirft einen TypeError bei ungültigen Typen oder leerem Namen!
*
* @param {string | number} id
* @param {string} name
* @param {boolean} [istAktiv=true]
* @param {string} [email=null]
* @returns {Kunde}
*/
function erstelleKunde(id, name, istAktiv = true, email = null) {
// TODO: Validierung & Erstellung des Kunden-Objekts implementieren
return null;
}
/**
* @typedef {Object} Auftragsposition
* @property {string} titel
* @property {number} menge
* @property {number} einzelpreis
*/
/**
* @typedef {'offen' | 'in_bearbeitung' | 'abgeschlossen' | 'storniert'} AuftragsStatus
*/
/**
* @typedef {Object} Auftrag
* @property {string | number} id
* @property {string | number} kundeId
* @property {Auftragsposition[]} positionen
* @property {number} summe
* @property {AuftragsStatus} status
*/
/**
* @typedef {Object} ValidierungsErgebnis
* @property {boolean} gueltig
* @property {string[]} fehler
*/
/**
* 🎯 TEILZIEL 2 (TODO 2): validiereAuftrag(auftragObjekt)
* Prüft ein Auftrag-Objekt auf vollständige Einhaltung aller Interface-Regeln:
* - Objekt vorhanden (nicht null/undefined)
* - id & kundeId vorhanden (string oder number)
* - positionen ist ein Array mit mind. 1 Element. Jedes Element hat:
* - titel (string, nicht leer)
* - menge (number > 0)
* - einzelpreis (number >= 0)
* - summe ist eine Zahl >= 0
* - status ist einer der erlaubten Werte: 'offen' | 'in_bearbeitung' | 'abgeschlossen' | 'storniert'
*
* @param {any} auftragObjekt
* @returns {ValidierungsErgebnis} { gueltig: boolean, fehler: string[] }
*/
function validiereAuftrag(auftragObjekt) {
// TODO: Typprüfung und Validierungsregeln implementieren
return { gueltig: false, fehler: ["Noch nicht implementiert"] };
}
/**
* 🎯 TEILZIEL 3 (TODO 3): filtereNachStatus(eintraege, zielStatus)
* Generische Filterfunktion: Filtert eine Liste beliebiger Objekte nach ihrem status-Feld.
*
* Anforderungen:
* - Wenn eintraege kein Array ist, leeres Array [] zurückgeben.
* - Gibt alle Elemente zurück, deren item.status exakt mit zielStatus übereinstimmt.
*
* @template T
* @param {T[]} eintraege
* @param {string} zielStatus
* @returns {T[]}
*/
function filtereNachStatus(eintraege, zielStatus) {
// TODO: Generische Filterung implementieren
return [];
}
/**
* 🎯 TEILZIEL 4 (TODO 4): formatierePreisangabe(betrag, waehrung = "EUR")
* Formatiert einen numerischen Geldbetrag mit 2 Nachkommastellen und Währungssymbol.
*
* Anforderungen:
* - betrag muss eine gültige Zahl sein (typeof === 'number' und !isNaN)
* - waehrung muss ein nicht-leerer String sein (Standard: "EUR")
* - Deutsches Format mit Komma als Dezimaltrenner: z.B. 49.99 -> "49,99 EUR"
* - Wirft TypeError bei ungültigem Betrag oder ungültiger Währung.
*
* @param {number} betrag
* @param {string} [waehrung="EUR"]
* @returns {string} z.B. "49,99 EUR"
*/
function formatierePreisangabe(betrag, waehrung = "EUR") {
// TODO: Formatierung und Typprüfung implementieren
return "";
}

View file

@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TS 07: TypeScript Type Safety &amp; Interfaces</title>
<link rel="stylesheet" href="../../../assets/style.css">
</head>
<body>
<div class="app-layout">
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🛡️ TypeScript &amp; Type Safety</div>
<h1>TS 07: TypeScript Type Safety &amp; Interfaces</h1>
<p>Entdecke die Macht statischer Typen: Interfaces, Union Types, Generics und robuster Code ohne Laufzeit-Überraschungen.</p>
</div>
</header>
<main class="container">
<!-- 📂 Schnellzugriff & Download -->
<div style="background: #ffffff; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; box-shadow: var(--shadow-sm);">
<div style="font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 8px;">
<span>📂 Dateien dieser Einheit:</span>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="aufgabe.js" class="btn" style="padding: 6px 14px; font-size: 0.88rem; background: var(--primary);" download>📄 aufgabe.js (Startercode)</a>
<a href="test_aufgabe.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>🧪 test_aufgabe.js (Tests)</a>
<a href="musterloesung.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>💡 musterloesung.js (Lösung)</a>
<a href="README.md" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;">📖 README.md</a>
</div>
</div>
<!-- 1. Didaktisches Intro -->
<section class="card">
<h2>1. 🛡️ Warum TypeScript? Der digitale Sicherheitsgurt</h2>
<p>
In reinem JavaScript treten Typfehler oft erst spät zur Laufzeit auf – beispielsweise wenn ein Kunde im Online-Shop auf „Kaufen“ klickt und das Programm mit <code>TypeError: Cannot read properties of undefined</code> abstürzt.
</p>
<p>
<strong>TypeScript</strong> erweitert JavaScript um statische Typüberprüfungen. Es wirkt wie ein <strong>digitaler Sicherheitsgurt</strong> und ein unbestechlicher <strong>Bauplan-Wächter</strong>: Jeder Parameter, jedes Objekt und jede Schnittstelle wird genau geprüft, noch bevor der Code im Browser ausgeführt wird.
</p>
<div class="box box-analogie">
<div class="box-title">🏗️ Die Architekten-Analogie:</div>
JavaScript ohne Typen ist wie eine Baustelle ohne Plan: Man merkt erst beim Einzug, dass das Treppenhaus fehlt. TypeScript liefert den geprüften Bauplan: Passt ein Baustein nicht, warnt dich der Compiler sofort im Editor!
</div>
</section>
<!-- 2. Cheat Sheet: Basis-Typen & Union Types -->
<section class="card">
<h2>2. 📋 TypeScript Cheat-Sheet: Basis-Typen &amp; Union Types</h2>
<p>TypeScript bietet alle vertrauten Datentypen sowie mächtige Kompositionswerkzeuge:</p>
<div class="code-container">
<div class="code-header">
<div class="code-dots">
<span class="code-dot dot-red"></span>
<span class="code-dot dot-yellow"></span>
<span class="code-dot dot-green"></span>
</div>
<span>TypeScript (Basis-Typen &amp; Type Aliases)</span>
</div>
<pre><span class="comment">// 1. Primitive Typen</span>
<span class="kw">const</span> <span class="var">name</span>: <span class="cls">string</span> = <span class="str">"Max Mustermann"</span>;
<span class="kw">const</span> <span class="var">alter</span>: <span class="cls">number</span> = <span class="num">34</span>;
<span class="kw">const</span> <span class="var">aktiv</span>: <span class="cls">boolean</span> = <span class="kw">true</span>;
<span class="kw">const</span> <span class="var">hobbies</span>: <span class="cls">string[]</span> = [<span class="str">"Coding"</span>, <span class="str">"Design"</span>];
<span class="comment">// 2. Type Aliases &amp; Union Types (|)</span>
<span class="kw">type</span> <span class="cls">ID</span> = <span class="cls">string</span> | <span class="cls">number</span>;
<span class="kw">type</span> <span class="cls">Status</span> = <span class="str">"offen"</span> | <span class="str">"in_bearbeitung"</span> | <span class="str">"abgeschlossen"</span> | <span class="str">"storniert"</span>;
<span class="comment">// 3. Spezialtypen</span>
<span class="kw">let</span> <span class="var">beliebig</span>: <span class="cls">any</span> = <span class="str">"Gefährlich: Deaktiviert alle Checks"</span>;
<span class="kw">let</span> <span class="var">sicherer</span>: <span class="cls">unknown</span> = JSON.<span class="fn">parse</span>(daten); <span class="comment">// Erfordert Prüfung vor Zugriff</span>
<span class="kw">function</span> <span class="fn">loggen</span>(<span class="var">msg</span>: <span class="cls">string</span>): <span class="cls">void</span> { console.<span class="fn">log</span>(<span class="var">msg</span>); }</pre>
</div>
</section>
<!-- 3. Interfaces & Diagramm -->
<section class="card">
<h2>3. 📐 Interfaces: Verträge für Datenstrukturen</h2>
<p>Ein <code>interface</code> beschreibt die exakte Form von Objekten. Optionale Eigenschaften werden mit <code>?</code> gekennzeichnet:</p>
<div style="background: #0f172a; border: 1px solid #334155; border-radius: var(--radius-sm); padding: 16px; margin: 15px 0; color: #f8fafc; font-family: var(--font-mono); font-size: 0.88rem; line-height: 1.6;">
<div style="color: #38bdf8; font-weight: 700; margin-bottom: 8px;">📊 Interface-Diagramm: Auftrag &amp; Positionen</div>
<div style="border-left: 2px solid #38bdf8; padding-left: 12px; margin-bottom: 12px;">
<span style="color: #fbbf24;">interface Auftrag</span> {<br>
&nbsp;&nbsp;id: <span style="color: #a78bfa;">string | number</span>;<br>
&nbsp;&nbsp;kundeId: <span style="color: #a78bfa;">string | number</span>;<br>
&nbsp;&nbsp;positionen: <span style="color: #34d399;">Auftragsposition[]</span>;<br>
&nbsp;&nbsp;summe: <span style="color: #a78bfa;">number</span>;<br>
&nbsp;&nbsp;status: <span style="color: #f472b6;">AuftragsStatus</span>;<br>
}
</div>
<div style="border-left: 2px solid #34d399; padding-left: 12px;">
<span style="color: #fbbf24;">interface Auftragsposition</span> {<br>
&nbsp;&nbsp;titel: <span style="color: #a78bfa;">string</span>;<br>
&nbsp;&nbsp;menge: <span style="color: #a78bfa;">number</span>;<br>
&nbsp;&nbsp;einzelpreis: <span style="color: #a78bfa;">number</span>;<br>
}
</div>
</div>
<div class="box box-tipp">
💡 <strong>Optional Chaining &amp; Nullish Coalescing:</strong><br>
Nutze <code>kunde?.adresse?.stadt ?? 'Unbekannt'</code>, um verschachtelte Eigenschaften sicher abzurufen, ohne dass Fehler bei <code>null</code> oder <code>undefined</code> auftreten.
</div>
</section>
<!-- 4. Generics -->
<section class="card">
<h2>4. 🧬 Generics: Typ-Parameter für flexible Funktionen</h2>
<p>
Mit <strong>Generics</strong> (<code>&lt;T&gt;</code>) schreibst du Funktionen, die für beliebige Typen funktionieren, ohne den Typenverlust von <code>any</code>:
</p>
<pre><code>function umkehren&lt;T&gt;(liste: T[]): T[] {
return [...liste].reverse();
}
const zahlen = umkehren([1, 2, 3]); // Typ bleibt number[]
const strings = umkehren(["A", "B", "C"]); // Typ bleibt string[]</code></pre>
</section>
<!-- 5. Aufgaben & Checkliste -->
<section class="card">
<h2>5. 🎯 Aufgaben &amp; Checkliste</h2>
<p>Öffne die Datei <code><a href="aufgabe.js" style="color: inherit; text-decoration: underline;">aufgabe.js</a></code> und setze folgende 4 Teilziele um:</p>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t1">
<label class="task-label" for="t1">
<strong>TODO 1:</strong> <code>erstelleKunde(id, name, istAktiv, email)</code> – Typprüfung, .trim() und Erstellung des Kunden-Objekts.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t2">
<label class="task-label" for="t2">
<strong>TODO 2:</strong> <code>validiereAuftrag(auftragObjekt)</code> – Vollständige Validierung gegen das Auftrags-Interface mit Fehler-Array.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t3">
<label class="task-label" for="t3">
<strong>TODO 3:</strong> <code>filtereNachStatus(eintraege, zielStatus)</code> – Generische Filterfunktion mit Fallback auf leeres Array.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t4">
<label class="task-label" for="t4">
<strong>TODO 4:</strong> <code>formatierePreisangabe(betrag, waehrung)</code> – Deutsches Format (z.B. <code>49,99 EUR</code>) mit Type-Check.
</label>
</div>
<div style="margin-top: 24px;">
<a href="../../../workspace.html?course=javascript&track=track_3_typescript_und_master&chapter=07_einstieg_typescript" class="btn" style="background: #3178c6; color: #fff; font-weight: 700; padding: 10px 20px;">💻 In Web-IDE öffnen &amp; Tests starten &rarr;</a>
</div>
</section>
<!-- Footer Navigation -->
<div class="footer-nav" style="display: flex; justify-content: space-between; margin-top: 30px;">
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">&larr; Zurück zu Modul 02</a>
<a href="../08_abschlussprojekt_web_app/index.html" class="btn" style="background: var(--primary);">Weiter zum Master-Projekt Modul 08 &rarr;</a>
</div>
</main>
</div>
</div>
<script src="../../../assets/ui_enhancements.js"></script>
</body>
</html>

View file

@ -0,0 +1,132 @@
/**
* 🌐 JS/TS 07: TYPESCRIPT TYPE SAFETY & INTERFACES (MUSTERLÖSUNG) 🌐
* ==================================================================
*/
/**
* @typedef {Object} Kunde
* @property {string | number} id - Eindeutige Kunden-ID
* @property {string} name - Vollständiger Name des Kunden
* @property {boolean} istAktiv - Status des Kunden
* @property {string} [email] - Optionale E-Mail-Adresse
* @property {string} erstelltAm - ISO-Datumsstring der Erstellung
*/
/**
* 🎯 TEILZIEL 1: erstelleKunde(id, name, istAktiv = true, email = null)
*/
function erstelleKunde(id, name, istAktiv = true, email = null) {
if (id === null || id === undefined || id === "" || (typeof id !== "string" && typeof id !== "number")) {
throw new TypeError("Ungültige Kunden-ID: string oder number erwartet");
}
if (typeof name !== "string" || name.trim().length === 0) {
throw new TypeError("Ungültiger Kunden-Name: nicht-leerer String erwartet");
}
if (typeof istAktiv !== "boolean") {
istAktiv = Boolean(istAktiv);
}
if (email !== null && email !== undefined && (typeof email !== "string" || email.trim() === "")) {
throw new TypeError("Ungültige E-Mail-Adresse");
}
const kunde = {
id: id,
name: name.trim(),
istAktiv: istAktiv,
erstelltAm: new Date().toISOString()
};
if (email && typeof email === "string" && email.trim().length > 0) {
kunde.email = email.trim();
}
return kunde;
}
/**
* 🎯 TEILZIEL 2: validiereAuftrag(auftragObjekt)
*/
function validiereAuftrag(auftragObjekt) {
if (!auftragObjekt || typeof auftragObjekt !== "object" || Array.isArray(auftragObjekt)) {
return { gueltig: false, fehler: ["Auftrag muss ein gültiges Objekt sein"] };
}
const fehler = [];
// ID Validierung
if (auftragObjekt.id === undefined || auftragObjekt.id === null || auftragObjekt.id === "" ||
(typeof auftragObjekt.id !== "string" && typeof auftragObjekt.id !== "number")) {
fehler.push("Ungültige Auftrags-ID");
}
// Kunde-ID Validierung
if (auftragObjekt.kundeId === undefined || auftragObjekt.kundeId === null || auftragObjekt.kundeId === "" ||
(typeof auftragObjekt.kundeId !== "string" && typeof auftragObjekt.kundeId !== "number")) {
fehler.push("Ungültige Kunden-ID");
}
// Positionen Validierung
if (!Array.isArray(auftragObjekt.positionen) || auftragObjekt.positionen.length === 0) {
fehler.push("Mindestens eine Position erforderlich");
} else {
auftragObjekt.positionen.forEach((pos, idx) => {
if (!pos || typeof pos !== "object") {
fehler.push(`Position ${idx + 1} ist kein gültiges Objekt`);
} else {
if (typeof pos.titel !== "string" || pos.titel.trim() === "") {
fehler.push(`Position ${idx + 1}: Titel fehlt oder ist leer`);
}
if (typeof pos.menge !== "number" || isNaN(pos.menge) || pos.menge <= 0) {
fehler.push(`Position ${idx + 1}: Menge muss > 0 sein`);
}
if (typeof pos.einzelpreis !== "number" || isNaN(pos.einzelpreis) || pos.einzelpreis < 0) {
fehler.push(`Position ${idx + 1}: Einzelpreis muss >= 0 sein`);
}
}
});
}
// Summen-Validierung
if (typeof auftragObjekt.summe !== "number" || isNaN(auftragObjekt.summe) || auftragObjekt.summe < 0) {
fehler.push("Summe muss eine Zahl >= 0 sein");
}
// Status Validierung
const erlaubteStatus = ["offen", "in_bearbeitung", "abgeschlossen", "storniert"];
if (!erlaubteStatus.includes(auftragObjekt.status)) {
fehler.push("Status ungültig (erlaubt: offen, in_bearbeitung, abgeschlossen, storniert)");
}
return {
gueltig: fehler.length === 0,
fehler: fehler
};
}
/**
* 🎯 TEILZIEL 3: filtereNachStatus(eintraege, zielStatus)
*/
function filtereNachStatus(eintraege, zielStatus) {
if (!Array.isArray(eintraege)) {
return [];
}
return eintraege.filter(item => item && typeof item === "object" && item.status === zielStatus);
}
/**
* 🎯 TEILZIEL 4: formatierePreisangabe(betrag, waehrung = "EUR")
*/
function formatierePreisangabe(betrag, waehrung = "EUR") {
if (typeof betrag !== "number" || isNaN(betrag)) {
throw new TypeError("Ungültiger Betrag: Zahl erwartet");
}
if (typeof waehrung !== "string" || waehrung.trim().length === 0) {
throw new TypeError("Ungültige Währung: nicht-leerer String erwartet");
}
const formatierteZahl = betrag.toFixed(2).replace(".", ",");
return `${formatierteZahl} ${waehrung.trim().toUpperCase()}`;
}

View file

@ -0,0 +1,105 @@
/**
* 🧪 Testsuite für JS/TS 07: TypeScript Type Safety & Interfaces
*/
// Helper für Exception-Prüfung in Sandbox & Node
function assertThrows(fn, msg) {
let threw = false;
try {
fn();
} catch (e) {
threw = true;
}
assert.ok(threw, msg || "Erwartete Exception wurde nicht geworfen!");
}
// ----------------------------------------------------
// 1. Tests für erstelleKunde(id, name, istAktiv, email)
// ----------------------------------------------------
const k1 = erstelleKunde(101, "Max Mustermann");
assert.strictEqual(k1.id, 101, "Kunden-ID sollte 101 sein");
assert.strictEqual(k1.name, "Max Mustermann", "Name sollte 'Max Mustermann' sein");
assert.strictEqual(k1.istAktiv, true, "Standardwert für istAktiv sollte true sein");
assert.ok(typeof k1.erstelltAm === "string" && k1.erstelltAm.length > 0, "erstelltAm muss ein Datums-String sein");
const k2 = erstelleKunde("C-42", " Lisa Schmidt ", false, "lisa@example.com");
assert.strictEqual(k2.id, "C-42", "Kunden-ID sollte 'C-42' sein");
assert.strictEqual(k2.name, "Lisa Schmidt", "Name sollte mit .trim() bereinigt werden");
assert.strictEqual(k2.istAktiv, false, "istAktiv sollte false sein");
assert.strictEqual(k2.email, "lisa@example.com", "E-Mail sollte übernommen werden");
assertThrows(() => erstelleKunde(null, "Test"), "erstelleKunde mit id=null muss Fehler werfen");
assertThrows(() => erstelleKunde(10, ""), "erstelleKunde mit leerem Namen muss Fehler werfen");
assertThrows(() => erstelleKunde(10, " "), "erstelleKunde mit Leerzeichen-Name muss Fehler werfen");
assertThrows(() => erstelleKunde(10, 12345), "erstelleKunde mit Zahl als Name muss Fehler werfen");
// ----------------------------------------------------
// 2. Tests für validiereAuftrag(auftragObjekt)
// ----------------------------------------------------
const gueltigerAuftrag = {
id: "A-1001",
kundeId: 101,
positionen: [
{ titel: "Webdesign & Frontend", menge: 1, einzelpreis: 1200.00 },
{ titel: "Hosting Setup", menge: 2, einzelpreis: 50.00 }
],
summe: 1300.00,
status: "offen"
};
const res1 = validiereAuftrag(gueltigerAuftrag);
assert.strictEqual(res1.gueltig, true, "Gültiger Auftrag sollte als gueltig: true erkannt werden");
assert.strictEqual(res1.fehler.length, 0, "Gültiger Auftrag darf keine Fehlermeldungen haben");
const ungueltigerStatus = { ...gueltigerAuftrag, status: "in_prüfung" };
const res2 = validiereAuftrag(ungueltigerStatus);
assert.strictEqual(res2.gueltig, false, "Unbekannter Status muss als ungültig erkannt werden");
const keinePositionen = { ...gueltigerAuftrag, positionen: [] };
const res3 = validiereAuftrag(keinePositionen);
assert.strictEqual(res3.gueltig, false, "Auftrag ohne Positionen muss ungültig sein");
const fehlerhaftePosition = {
...gueltigerAuftrag,
positionen: [{ titel: "", menge: -1, einzelpreis: 10 }]
};
const res4 = validiereAuftrag(fehlerhaftePosition);
assert.strictEqual(res4.gueltig, false, "Ungültige Positionen müssen erkannt werden");
const resNull = validiereAuftrag(null);
assert.strictEqual(resNull.gueltig, false, "null als Auftrag muss abgefangen werden");
// ----------------------------------------------------
// 3. Tests für filtereNachStatus(eintraege, zielStatus)
// ----------------------------------------------------
const liste = [
{ id: 1, titel: "Design", status: "offen" },
{ id: 2, titel: "Backend", status: "in_bearbeitung" },
{ id: 3, titel: "Deployment", status: "offen" },
{ id: 4, titel: "Testing", status: "abgeschlossen" }
];
const offene = filtereNachStatus(liste, "offen");
assert.strictEqual(offene.length, 2, "Es sollte genau 2 offene Einträge geben");
assert.strictEqual(offene[0].id, 1, "Erster gefilterter Eintrag ID");
assert.strictEqual(offene[1].id, 3, "Zweiter gefilterter Eintrag ID");
const abgeschlossene = filtereNachStatus(liste, "abgeschlossen");
assert.strictEqual(abgeschlossene.length, 1, "Es sollte 1 abgeschlossenen Eintrag geben");
const stornierte = filtereNachStatus(liste, "storniert");
assert.strictEqual(stornierte.length, 0, "Nicht vorhandener Status liefert leeres Array");
assert.deepStrictEqual(filtereNachStatus(null, "offen"), [], "Ungültige Liste liefert []");
// ----------------------------------------------------
// 4. Tests für formatierePreisangabe(betrag, waehrung)
// ----------------------------------------------------
assert.strictEqual(formatierePreisangabe(49.99), "49,99 EUR", "formatierePreisangabe(49.99) mit Default EUR");
assert.strictEqual(formatierePreisangabe(1250, "USD"), "1250,00 USD", "formatierePreisangabe(1250, 'USD')");
assert.strictEqual(formatierePreisangabe(0, "chf"), "0,00 CHF", "Währung sollte in Großbuchstaben formatiert werden");
assert.strictEqual(formatierePreisangabe(9.5, "EUR"), "9,50 EUR", "Einstellige Nachkommastelle auf 2 Stellen auffüllen");
assertThrows(() => formatierePreisangabe("49.99"), "String als Betrag muss TypeError werfen");
assertThrows(() => formatierePreisangabe(NaN), "NaN als Betrag muss TypeError werfen");
assertThrows(() => formatierePreisangabe(10, ""), "Leere Währung muss TypeError werfen");
console.log("✅ Alle Tests für TS 07 (TypeScript Type Safety & Interfaces) erfolgreich bestanden!");

View file

@ -0,0 +1,112 @@
# 🏆 Modul 08: Master-Abschlussprojekt – Interaktive Task-App (MVC)
> **Leitgedanke:** *„Ein wahrer Software-Engineer zeichnet sich nicht nur dadurch aus, einzelne Funktionen zu schreiben, sondern komplexe Systeme mit klarer Model-View-Architektur, State-Management und 100% testgetriebener Logik zu strukturieren.“*
---
## 📚 Übersicht & Projektziel
Herzlichen Glückwunsch zum Erreichen des **Master-Abschlussprojekts** des JavaScript & TypeScript Kurses! In diesem Abschlussprojekt entwickelst du eine vollständige, professionelle Aufgaben- und Projektmanagement-Web-Applikation.
### Kernanforderungen:
1. **Saubere Model-View-Trennung (MVC / Clean Architecture)**:
- Die Geschäftslogik (`TaskManager`) ist zu 100% unabhängig vom DOM und läuft headless in jedem Test-Runner.
- Die View (`TaskApp`) reagiert reaktiv auf State-Änderungen und visualisiert Tasks mit Prioritäts-Badges und Fälligkeitsdaten.
2. **Umfassende Aufgabenverwaltung**:
- Aufgaben hinzufügen mit Titel, Priorität (`high`, `medium`, `low`) und Fälligkeitsdatum (`dueDate`).
- Status umschalten (Toggle: Offen <-> Erledigt).
- Aufgaben löschen und abgeschlossene Aufgaben gesammelt aufräumen.
- Dynamische Filterung (`all`, `active`, `completed`) und Volltextsuche (Live Search).
- Aggregat-Statistiken in Echtzeit (Gesamtzahl, Offen, Erledigt, Hohe Priorität).
3. **100% Unittest-Abdeckung** mit automatisierter Testsuite.
---
## 🏛️ 1. Architektur: Model-View-Controller (MVC)
```
┌─────────────────────────────────────────────────────────────┐
│ BENUTZER / BROWSER UI │
└──────────────────────────────┬──────────────────────────────┘
│ Klick / Formular-Eingabe
▼
┌─────────────────────────────────────────────────────────────┐
│ VIEW / CONTROLLER (app.js) │
│ - Reagiert auf DOM-Events (submit, click, input) │
│ - Aktualisiert das DOM bei State-Änderungen │
│ - Formatiert UI-Elemente (Badges, Datumsanzeige) │
└──────────────────────────────┬──────────────────────────────┘
│ Ruft Methoden auf
▼
┌─────────────────────────────────────────────────────────────┐
│ MODEL / STATE (TaskManager) │
│ - addTask(title, priority, dueDate) │
│ - toggleTask(id) │
│ - deleteTask(id) │
│ - getFilteredTasks(status, query) │
│ - getStatistics() │
└─────────────────────────────────────────────────────────────┘
```
---
## 📐 2. TypeScript Datenmodell & Interfaces
```typescript
type TaskPriority = "high" | "medium" | "low";
type FilterStatus = "all" | "active" | "completed";
interface Task {
id: number;
title: string;
priority: TaskPriority;
completed: boolean;
dueDate: string | null;
createdAt: string;
}
interface TaskStatistics {
total: number;
active: number;
completed: number;
highPriorityCount: number;
}
```
---
## 🎯 3. Deine Aufgaben in `aufgabe.js`
Implementiere die Klasse `TaskManager`:
### 🎯 TODO 1: `constructor(initialTasks = [])` & `addTask(title, priority = 'medium', dueDate = null)`
- Erzeugt eine neue Aufgabe mit fortlaufender ID (1, 2, 3, ...).
- Validiert den Titel (muss ein nicht-leerer String sein; wirft sonst einen `Error`).
- Setzt Standardwerte für `priority` ('medium') und `dueDate` (null).
- Gibt das neu erstellte `Task`-Objekt zurück.
### 🎯 TODO 2: `toggleTask(id)`
- Schaltet `completed` für die Aufgabe mit der angegebenen ID um (`true` <-> `false`).
- Liefert den aktualisierten Task zurück, oder `null` falls die ID nicht gefunden wurde.
### 🎯 TODO 3: `deleteTask(id)`
- Entfernt die Aufgabe mit der ID aus der internen Liste.
- Gibt `true` bei erfolgreichem Löschen zurück, sonst `false`.
### 🎯 TODO 4: `getFilteredTasks(filterStatus = 'all', searchQuery = '')`
- Filtert nach Status: `'all'` (alle), `'active'` (nur unerledigte), `'completed'` (nur erledigte).
- Filtert optional case-insensitive nach Teilübereinstimmung im Titel (`searchQuery`).
- Gibt eine Kopie des gefilterten Arrays zurück.
### 🎯 TODO 5: `getStatistics()`
- Liefert ein Objekt mit `{ total, active, completed, highPriorityCount }`.
---
## 🧪 4. Verifikation & Ausführung
### Unittests ausführen:
Öffne das Modul in der integrierten Web-IDE oder führe `test_aufgabe.js` aus.
### Interaktive Browser-App testen:
Öffne `index.html` direkt im Browser, um die live gerenderte Task-App auszuprobieren!

View file

@ -0,0 +1,295 @@
/**
* 🌐 MASTER 08: INTERAKTIVE TASK-APP (VIEW & CONTROLLER) 🌐
* ==========================================================
* Verbindet die TaskManager-Geschäftslogik mit einer modernen,
* reaktiven Benutzeroberfläche im Browser.
*/
// Falls TaskManager noch nicht global vorhanden ist (z.B. Standalone):
if (typeof TaskManager === "undefined") {
// Fallback Minimal-Implementation oder Import
console.warn("TaskManager nicht gefunden. Lade Fallback-Definition.");
}
class TaskApp {
constructor(rootElementId = "app") {
this.root = document.getElementById(rootElementId) || document.body;
this.manager = new TaskManager([
{ id: 1, title: "TypeScript Interfaces & Union Types verstehen", priority: "high", completed: true, dueDate: "2026-08-30", createdAt: new Date().toISOString() },
{ id: 2, title: "Model-View-Controller Architektur im Web umsetzen", priority: "high", completed: false, dueDate: "2026-09-01", createdAt: new Date().toISOString() },
{ id: 3, title: "Design System & Dark Mode Akzente optimieren", priority: "medium", completed: false, dueDate: "2026-09-05", createdAt: new Date().toISOString() },
{ id: 4, title: "Automatisierte Unittests mit 100% Abdeckung schreiben", priority: "low", completed: false, dueDate: null, createdAt: new Date().toISOString() }
]);
this.currentFilter = "all";
this.searchQuery = "";
this.init();
}
init() {
this.renderLayout();
this.attachEventListeners();
this.updateView();
}
renderLayout() {
this.root.innerHTML = `
<div class="task-app-wrapper" style="max-width: 820px; margin: 0 auto; font-family: var(--font-sans, system-ui, sans-serif);">
<!-- App Header & Stats Bar -->
<div style="background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%); color: white; padding: 24px; border-radius: 12px; margin-bottom: 24px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 16px;">
<div>
<h2 style="margin: 0; font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 8px;">
<span>🎯</span> Modern Task & Project Planner
</h2>
<p style="margin: 4px 0 0 0; font-size: 0.88rem; color: #c7d2fe;">Statussichere Aufgabenverwaltung mit TypeScript & Clean Architecture</p>
</div>
<button id="btn-clear-completed" class="btn" style="background: rgba(255,255,255,0.15); color: white; font-size: 0.82rem; padding: 6px 12px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; cursor: pointer;">
🧹 Erledigte aufräumen
</button>
</div>
<!-- Statistik-Karten -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px;" id="stats-container">
<div style="background: rgba(255,255,255,0.08); padding: 10px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
<div style="font-size: 0.72rem; text-transform: uppercase; color: #a5b4fc; font-weight: 700;">Gesamt</div>
<div style="font-size: 1.4rem; font-weight: 800;" id="stat-total">0</div>
</div>
<div style="background: rgba(255,255,255,0.08); padding: 10px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
<div style="font-size: 0.72rem; text-transform: uppercase; color: #fde047; font-weight: 700;">Aktiv / Offen</div>
<div style="font-size: 1.4rem; font-weight: 800; color: #fde047;" id="stat-active">0</div>
</div>
<div style="background: rgba(255,255,255,0.08); padding: 10px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
<div style="font-size: 0.72rem; text-transform: uppercase; color: #86efac; font-weight: 700;">Erledigt</div>
<div style="font-size: 1.4rem; font-weight: 800; color: #86efac;" id="stat-completed">0</div>
</div>
<div style="background: rgba(255,255,255,0.08); padding: 10px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
<div style="font-size: 0.72rem; text-transform: uppercase; color: #fca5a5; font-weight: 700;">🔥 Hohe Prio</div>
<div style="font-size: 1.4rem; font-weight: 800; color: #fca5a5;" id="stat-high">0</div>
</div>
</div>
</div>
<!-- Formular zum Hinzufügen -->
<div style="background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 18px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
<form id="task-form" style="display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<input type="text" id="task-input-title" placeholder="Was steht als Nächstes an? (z.B. API-Schnittstelle testen)" style="flex: 3; min-width: 220px; padding: 10px 14px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 0.95rem; outline: none;" required>
<select id="task-select-priority" style="flex: 1; min-width: 130px; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 0.9rem; background: white;">
<option value="high">🔥 Hohe Priorität</option>
<option value="medium" selected>⚡ Mittlere Priorität</option>
<option value="low">🌱 Niedrige Priorität</option>
</select>
<input type="date" id="task-input-due" style="flex: 1; min-width: 140px; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 0.9rem;">
<button type="submit" class="btn" style="background: #4f46e5; color: white; border: none; padding: 10px 18px; border-radius: 8px; font-weight: 700; cursor: pointer; white-space: nowrap;">
➕ Hinzufügen
</button>
</div>
</form>
</div>
<!-- Filter- & Suchleiste -->
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 16px;">
<div style="display: flex; gap: 6px;" id="filter-buttons">
<button data-filter="all" class="filter-btn active" style="padding: 6px 14px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; cursor: pointer; border: 1px solid #4f46e5; background: #4f46e5; color: white;">Alle</button>
<button data-filter="active" class="filter-btn" style="padding: 6px 14px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; cursor: pointer; border: 1px solid #cbd5e1; background: white; color: #475569;">Aktiv</button>
<button data-filter="completed" class="filter-btn" style="padding: 6px 14px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; cursor: pointer; border: 1px solid #cbd5e1; background: white; color: #475569;">Erledigt</button>
</div>
<div style="position: relative; min-width: 220px;">
<input type="text" id="search-input" placeholder="🔍 Aufgaben suchen..." style="width: 100%; padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 20px; font-size: 0.85rem; box-sizing: border-box; outline: none;">
</div>
</div>
<!-- Task-Liste Container -->
<div id="task-list" style="display: flex; flex-direction: column; gap: 8px;">
<!-- Dynamisch gerenderte Aufgaben -->
</div>
</div>
`;
}
attachEventListeners() {
// Formular-Submit
const form = document.getElementById("task-form");
if (form) {
form.addEventListener("submit", (e) => {
e.preventDefault();
const titleInput = document.getElementById("task-input-title");
const prioritySelect = document.getElementById("task-select-priority");
const dueInput = document.getElementById("task-input-due");
const title = titleInput.value.trim();
const priority = prioritySelect.value;
const dueDate = dueInput.value || null;
if (title) {
try {
this.manager.addTask(title, priority, dueDate);
titleInput.value = "";
dueInput.value = "";
this.updateView();
} catch (err) {
alert(err.message);
}
}
});
}
// Filter Buttons
const filterContainer = document.getElementById("filter-buttons");
if (filterContainer) {
filterContainer.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-filter]");
if (btn) {
this.currentFilter = btn.getAttribute("data-filter");
// Button-Stile aktualisieren
filterContainer.querySelectorAll(".filter-btn").forEach((b) => {
b.style.background = "white";
b.style.color = "#475569";
b.style.borderColor = "#cbd5e1";
});
btn.style.background = "#4f46e5";
btn.style.color = "white";
btn.style.borderColor = "#4f46e5";
this.updateView();
}
});
}
// Live-Suche
const searchInput = document.getElementById("search-input");
if (searchInput) {
searchInput.addEventListener("input", (e) => {
this.searchQuery = e.target.value;
this.updateView();
});
}
// Erledigte aufräumen
const clearBtn = document.getElementById("btn-clear-completed");
if (clearBtn) {
clearBtn.addEventListener("click", () => {
if (confirm("Möchtest du alle erledigten Aufgaben wirklich entfernen?")) {
this.manager.clearCompleted();
this.updateView();
}
});
}
// Task-Liste Delegation (Toggle & Delete)
const listContainer = document.getElementById("task-list");
if (listContainer) {
listContainer.addEventListener("click", (e) => {
const target = e.target;
const taskCard = target.closest("[data-task-id]");
if (!taskCard) return;
const id = Number(taskCard.getAttribute("data-task-id"));
if (target.classList.contains("btn-delete") || target.closest(".btn-delete")) {
this.manager.deleteTask(id);
this.updateView();
} else if (target.classList.contains("task-checkbox")) {
this.manager.toggleTask(id);
this.updateView();
}
});
}
}
updateView() {
this.renderStats();
this.renderTasks();
}
renderStats() {
const stats = this.manager.getStatistics();
const elTotal = document.getElementById("stat-total");
const elActive = document.getElementById("stat-active");
const elCompleted = document.getElementById("stat-completed");
const elHigh = document.getElementById("stat-high");
if (elTotal) elTotal.textContent = stats.total;
if (elActive) elActive.textContent = stats.active;
if (elCompleted) elCompleted.textContent = stats.completed;
if (elHigh) elHigh.textContent = stats.highPriorityCount;
}
renderTasks() {
const container = document.getElementById("task-list");
if (!container) return;
const filtered = this.manager.getFilteredTasks(this.currentFilter, this.searchQuery);
if (filtered.length === 0) {
container.innerHTML = `
<div style="background: white; border: 1px dashed #cbd5e1; border-radius: 10px; padding: 35px 20px; text-align: center; color: #64748b;">
<div style="font-size: 2rem; margin-bottom: 8px;">✨</div>
<div style="font-weight: 700; font-size: 1.05rem; color: #334155;">Keine Aufgaben gefunden</div>
<div style="font-size: 0.85rem; margin-top: 4px;">Alle Aufgaben erledigt oder kein Treffer für den aktuellen Filter.</div>
</div>
`;
return;
}
container.innerHTML = filtered
.map((t) => {
const priorityColors = {
high: { bg: "#fee2e2", text: "#991b1b", label: "🔥 Hoch" },
medium: { bg: "#fef3c7", text: "#92400e", label: "⚡ Mittel" },
low: { bg: "#dcfce7", text: "#166534", label: "🌱 Niedrig" }
};
const p = priorityColors[t.priority] || priorityColors.medium;
const dueHtml = t.dueDate
? `<span style="font-size: 0.75rem; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px; display: inline-flex; align-items: center; gap: 4px;">
📅 ${t.dueDate}
</span>`
: "";
return `
<div data-task-id="${t.id}" style="background: white; border: 1px solid ${t.completed ? "#e2e8f0" : "#cbd5e1"}; border-left: 5px solid ${t.priority === "high" ? "#ef4444" : t.priority === "medium" ? "#f59e0b" : "#10b981"}; border-radius: 8px; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; transition: all 0.2s; ${t.completed ? "opacity: 0.65; background: #f8fafc;" : ""}">
<div style="display: flex; align-items: center; gap: 12px; flex: 1;">
<input type="checkbox" class="task-checkbox" ${t.completed ? "checked" : ""} style="width: 18px; height: 18px; cursor: pointer; accent-color: #4f46e5;">
<div style="flex: 1;">
<div style="font-weight: 600; font-size: 0.95rem; color: ${t.completed ? "#64748b" : "#0f172a"}; ${t.completed ? "text-decoration: line-through;" : ""}">
${this.escapeHtml(t.title)}
</div>
<div style="display: flex; gap: 8px; align-items: center; margin-top: 4px; flex-wrap: wrap;">
<span style="font-size: 0.72rem; font-weight: 700; background: ${p.bg}; color: ${p.text}; padding: 2px 8px; border-radius: 99px;">
${p.label}
</span>
${dueHtml}
</div>
</div>
</div>
<button class="btn-delete" style="background: none; border: none; cursor: pointer; font-size: 1.1rem; padding: 4px 8px; border-radius: 4px; color: #94a3b8; transition: color 0.15s;" title="Aufgabe löschen">
🗑️
</button>
</div>
`;
})
.join("");
}
escapeHtml(str) {
if (!str) return "";
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
}
// Bei DOMContentLoaded automatisch starten wenn im Browser
if (typeof document !== "undefined") {
document.addEventListener("DOMContentLoaded", () => {
if (document.getElementById("app") || document.getElementById("task-app-root")) {
new TaskApp(document.getElementById("task-app-root") ? "task-app-root" : "app");
}
});
}

View file

@ -0,0 +1,145 @@
/**
* 🌐 MASTER 08: INTERAKTIVE TASK-APP (STATE & BUSINESS LOGIC) 🌐
* ===============================================================
* In diesem Master-Abschlussprojekt erstellst du die vollständige
* Geschäftslogik (Model & State Manager) für eine interaktive
* Aufgabenverwaltungs-Web-App mit sauberer Model-View Trennung.
*/
/**
* @typedef {'high' | 'medium' | 'low'} TaskPriority
* @typedef {'all' | 'active' | 'completed'} FilterStatus
*
* @typedef {Object} Task
* @property {number} id - Eindeutige numerische ID
* @property {string} title - Titel der Aufgabe
* @property {TaskPriority} priority - Priorität ('high', 'medium', 'low')
* @property {boolean} completed - Erledigungsstatus
* @property {string | null} dueDate - Optionales Fälligkeitsdatum (z.B. '2026-12-31')
* @property {string} createdAt - ISO-Datumsstring
*
* @typedef {Object} TaskStatistics
* @property {number} total - Gesamtzahl aller Aufgaben
* @property {number} active - Anzahl aktiver (unerledigter) Aufgaben
* @property {number} completed - Anzahl erledigter Aufgaben
* @property {number} highPriorityCount - Anzahl Aufgaben mit Priorität 'high'
*/
class TaskManager {
constructor(initialTasks = []) {
/** @type {Task[]} */
this.tasks = [];
this.nextId = 1;
// TODO: Initialisiere das Array und lade eventuelle initialTasks
}
/**
* 🎯 TEILZIEL 1 (TODO 1): addTask(title, priority = 'medium', dueDate = null)
* Fügt eine neue Aufgabe hinzu.
*
* Anforderungen:
* - title: string (nach .trim() nicht leer, sonst Error werfen)
* - priority: 'high' | 'medium' | 'low' (Standard: 'medium')
* - dueDate: string | null (Standard: null)
* - completed: initial false
* - createdAt: new Date().toISOString()
* - Vergibt eine eindeutige, inkrementierte ID (1, 2, 3, ...)
* - Gibt das erstellte Task-Objekt zurück.
*
* @param {string} title
* @param {TaskPriority} [priority='medium']
* @param {string | null} [dueDate=null]
* @returns {Task}
*/
addTask(title, priority = "medium", dueDate = null) {
// TODO: Neue Aufgabe erstellen und im tasks-Array speichern
return null;
}
/**
* 🎯 TEILZIEL 2 (TODO 2): toggleTask(id)
* Schaltet den completed-Status der Aufgabe mit der übergebenen ID um (true <-> false).
*
* Anforderungen:
* - Findet die Aufgabe anhand der ID.
* - Schaltet task.completed um.
* - Gibt den aktualisierten Task zurück, oder null falls ID nicht existiert.
*
* @param {number} id
* @returns {Task | null}
*/
toggleTask(id) {
// TODO: Status umschalten
return null;
}
/**
* 🎯 TEILZIEL 3 (TODO 3): deleteTask(id)
* Entfernt eine Aufgabe anhand ihrer ID.
*
* Anforderungen:
* - Entfernt die Aufgabe aus this.tasks.
* - Gibt true zurück wenn gelöscht wurde, sonst false wenn ID nicht vorhanden war.
*
* @param {number} id
* @returns {boolean}
*/
deleteTask(id) {
// TODO: Aufgabe löschen
return false;
}
/**
* 🎯 TEILZIEL 4 (TODO 4): getFilteredTasks(filterStatus = 'all', searchQuery = '')
* Liefert gefilterte Aufgaben basierend auf Status und optionalem Suchbegriff.
*
* Anforderungen:
* - filterStatus:
* - 'all': Alle Aufgaben
* - 'active': Nur Aufgaben mit completed === false
* - 'completed': Nur Aufgaben mit completed === true
* - searchQuery: Filtert zusätzlich case-insensitive nach Teilübereinstimmung im title (wenn nicht leer)
* - Liefert eine neue Array-Kopie der gefilterten Aufgaben zurück.
*
* @param {FilterStatus} [filterStatus='all']
* @param {string} [searchQuery='']
* @returns {Task[]}
*/
getFilteredTasks(filterStatus = "all", searchQuery = "") {
// TODO: Filtern nach Status und Volltextsuche
return [];
}
/**
* 🎯 TEILZIEL 5 (TODO 5): getStatistics()
* Berechnet Aggregat-Statistiken über alle Aufgaben.
*
* Anforderungen:
* - total: Gesamtzahl aller Aufgaben
* - active: Anzahl noch nicht erledigter Aufgaben
* - completed: Anzahl erledigter Aufgaben
* - highPriorityCount: Anzahl aller Aufgaben mit priority === 'high'
*
* @returns {TaskStatistics}
*/
getStatistics() {
// TODO: Statistiken berechnen
return {
total: 0,
active: 0,
completed: 0,
highPriorityCount: 0
};
}
/**
* Hilfsmethode: Findet eine Aufgabe nach ID
* @param {number} id
* @returns {Task | null}
*/
getTaskById(id) {
const numId = Number(id);
return this.tasks.find(t => t.id === numId) || null;
}
}

View file

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Master 08: Interaktive Task-App (MVC &amp; Clean Architecture)</title>
<link rel="stylesheet" href="../../../assets/style.css">
</head>
<body>
<div class="app-layout">
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🏆 Master-Abschlussprojekt</div>
<h1>Master 08: Interaktive Task-App (MVC)</h1>
<p>Kombiniere TypeScript-Typsicherheit, State-Management und reaktive DOM-Manipulation zu einer professionellen Web-Applikation.</p>
</div>
</header>
<main class="container">
<!-- 📂 Schnellzugriff & Download -->
<div style="background: #ffffff; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; box-shadow: var(--shadow-sm);">
<div style="font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 8px;">
<span>📂 Dateien dieser Einheit:</span>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="aufgabe.js" class="btn" style="padding: 6px 14px; font-size: 0.88rem; background: var(--primary);" download>📄 aufgabe.js (Startercode)</a>
<a href="test_aufgabe.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>🧪 test_aufgabe.js (Tests)</a>
<a href="musterloesung.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>💡 musterloesung.js (Lösung)</a>
<a href="app.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>🚀 app.js (Browser-App)</a>
<a href="README.md" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;">📖 README.md</a>
</div>
</div>
<!-- 1. Intro -->
<section class="card">
<h2>1. 🏆 Dein Meisterstück: Das Master-Abschlussprojekt</h2>
<p>
Herzlichen Glückwunsch! Du hast moderne JavaScript-Grundlagen, funktionale Array-Transformationen und statische Typsicherheit mit TypeScript gemeistert.
</p>
<p>
In diesem <strong>Master-Abschlussprojekt</strong> vereinst du alle Konzepte in einer professionell strukturierten Aufgabenverwaltungs-App. Das Projekt folgt dem <strong>Model-View-Controller (MVC)</strong>-Muster, dem Industriestandard moderner Web-Architektur.
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 15px; margin: 20px 0;">
<div style="background: #f0fdf4; border-left: 4px solid #10b981; padding: 14px; border-radius: var(--radius-sm);">
<h4 style="color: #166534; margin-bottom: 6px;">🌱 1. ES6+ &amp; Arrays</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">
Unveränderliche Arrays, Filterung mit <code>.filter()</code>, Suchen mit <code>.find()</code> / <code>.findIndex()</code> und Template Strings.
</p>
</div>
<div style="background: #eff6ff; border-left: 4px solid #3b82f6; padding: 14px; border-radius: var(--radius-sm);">
<h4 style="color: #1e40af; margin-bottom: 6px;">🛡️ 2. Type Safety &amp; Interfaces</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">
Definierte Datenstrukturen (<code>TaskPriority</code>, <code>FilterStatus</code>, <code>TaskStatistics</code>) und robuste Input-Validierung.
</p>
</div>
<div style="background: #f5f3ff; border-left: 4px solid #8b5cf6; padding: 14px; border-radius: var(--radius-sm);">
<h4 style="color: #5b21b6; margin-bottom: 6px;">🚀 3. State &amp; UI-Sync</h4>
<p style="font-size: 0.88rem; color: var(--text-muted); margin: 0;">
Zentrales State Management in der Klasse <code>TaskManager</code>, Event-Delegation und 100% Unittest-Abdeckung!
</p>
</div>
</div>
</section>
<!-- 2. MVC Architektur -->
<section class="card">
<h2>2. 🏛️ Die Architektur: Model-View-Controller (MVC)</h2>
<p>
Strikte Trennung von Geschäftslogik und Anzeige verhindert schwer wartbaren Code („Spaghetti-Code“):
</p>
<div class="mvc-grid">
<div class="mvc-card model">
<span class="badge-master" style="background: #10b981;">1. Das Model (State &amp; Logik)</span>
<h3 style="margin-top: 10px; color: #065f46;">Klasse <code>TaskManager</code></h3>
<p style="font-size: 0.9rem;">
Verwaltet das interne <code>tasks</code>-Array, vergibt IDs, schaltet Status um, filtert nach Kriterien und berechnet Aggregat-Statistiken.
</p>
<div class="box box-tipp" style="margin-top: 10px; font-size: 0.82rem; padding: 8px;">
💡 <strong>100% Headless:</strong> Funktioniert komplett ohne DOM im Testrunner!
</div>
</div>
<div class="mvc-card view">
<span class="badge-master" style="background: #3b82f6;">2. Die View (DOM-Renderer)</span>
<h3 style="margin-top: 10px; color: #1e40af;">HTML-Widgets &amp; Badges</h3>
<p style="font-size: 0.9rem;">
Rendert Formular, Status-Karten (Zähler), Filter-Buttons, Suchleiste und Task-Karten mit Prioritätsfarben.
</p>
<div class="box box-tipp" style="margin-top: 10px; font-size: 0.82rem; padding: 8px;">
👀 <strong>Dumb View:</strong> Zeigt nur den aktuellen State an.
</div>
</div>
<div class="mvc-card controller">
<span class="badge-master" style="background: #8b5cf6;">3. Der Controller (Event-Handler)</span>
<h3 style="margin-top: 10px; color: #5b21b6;">Klasse <code>TaskApp</code></h3>
<p style="font-size: 0.9rem;">
Lauscht auf Klicks, Tastatur-Eingaben und Form-Submits, delegiert Aktionen an den <code>TaskManager</code> und löst Neuzeichnungen aus.
</p>
<div class="box box-tipp" style="margin-top: 10px; font-size: 0.82rem; padding: 8px;">
🛡️ <strong>Sicher:</strong> Fängt Fehleingaben über Exceptions ab.
</div>
</div>
</div>
</section>
<!-- 3. Interaktive Live-Vorschau -->
<section class="card">
<h2>3. 🚀 Interaktive Live-Applikation</h2>
<p>So sieht die fertige Anwendung aus – teste sie hier direkt interaktiv aus:</p>
<div style="background: #f8fafc; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 20px; margin-top: 15px;">
<div id="task-app-root"></div>
</div>
</section>
<!-- 4. Aufgaben & Checkliste -->
<section class="card">
<h2>4. 🎯 Aufgaben &amp; Checkliste</h2>
<p>Öffne die Datei <code><a href="aufgabe.js" style="color: inherit; text-decoration: underline;">aufgabe.js</a></code> und implementiere die <code>TaskManager</code>-Klasse:</p>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t1">
<label class="task-label" for="t1">
<strong>TODO 1:</strong> <code>addTask(title, priority, dueDate)</code> – Validierung, ID-Vergabe (1, 2, ...), Default-Werte und Rückgabe des Tasks.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t2">
<label class="task-label" for="t2">
<strong>TODO 2:</strong> <code>toggleTask(id)</code> – Umschalten von <code>task.completed</code> (true <-> false) und Rückgabe oder <code>null</code>.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t3">
<label class="task-label" for="t3">
<strong>TODO 3:</strong> <code>deleteTask(id)</code> – Entfernen aus dem Array, Rückgabe <code>true</code> bei Erfolg, sonst <code>false</code>.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t4">
<label class="task-label" for="t4">
<strong>TODO 4:</strong> <code>getFilteredTasks(filterStatus, searchQuery)</code> – Filter nach 'all'/'active'/'completed' + Volltextsuche im Titel.
</label>
</div>
<div class="task-item">
<input type="checkbox" class="task-checkbox" id="t5">
<label class="task-label" for="t5">
<strong>TODO 5:</strong> <code>getStatistics()</code> – Rückgabe von <code>{ total, active, completed, highPriorityCount }</code>.
</label>
</div>
<div style="margin-top: 24px;">
<a href="../../../workspace.html?course=javascript&track=track_3_typescript_und_master&chapter=08_abschlussprojekt_web_app" class="btn" style="background: #4f46e5; color: #fff; font-weight: 700; padding: 10px 20px;">💻 In Web-IDE öffnen &amp; Unittests starten &rarr;</a>
</div>
</section>
<!-- Footer Navigation -->
<div class="footer-nav" style="display: flex; justify-content: space-between; margin-top: 30px;">
<a href="../07_einstieg_typescript/index.html" class="btn btn-secondary">&larr; Zurück zu Modul 07</a>
<a href="../../../index.html" class="btn" style="background: var(--success); color: white;">🏆 Kurs abgeschlossen &ndash; Zur Übersicht &rarr;</a>
</div>
</main>
</div>
</div>
<!-- Scripts für die Live-App -->
<script src="musterloesung.js"></script>
<script src="app.js"></script>
<script src="../../../assets/ui_enhancements.js"></script>
</body>
</html>

View file

@ -0,0 +1,154 @@
/**
* 🌐 MASTER 08: INTERAKTIVE TASK-APP (MUSTERLÖSUNG) 🌐
* ====================================================
*/
/**
* @typedef {'high' | 'medium' | 'low'} TaskPriority
* @typedef {'all' | 'active' | 'completed'} FilterStatus
*
* @typedef {Object} Task
* @property {number} id
* @property {string} title
* @property {TaskPriority} priority
* @property {boolean} completed
* @property {string | null} dueDate
* @property {string} createdAt
*
* @typedef {Object} TaskStatistics
* @property {number} total
* @property {number} active
* @property {number} completed
* @property {number} highPriorityCount
*/
class TaskManager {
constructor(initialTasks = []) {
/** @type {Task[]} */
this.tasks = [];
this.nextId = 1;
if (Array.isArray(initialTasks)) {
for (const t of initialTasks) {
if (t && typeof t === "object") {
const task = {
id: typeof t.id === "number" ? t.id : this.nextId++,
title: typeof t.title === "string" ? t.title.trim() : "Ohne Titel",
priority: ["high", "medium", "low"].includes(t.priority) ? t.priority : "medium",
completed: Boolean(t.completed),
dueDate: (typeof t.dueDate === "string" && t.dueDate.trim().length > 0) ? t.dueDate.trim() : null,
createdAt: t.createdAt || new Date().toISOString()
};
if (task.id >= this.nextId) {
this.nextId = task.id + 1;
}
this.tasks.push(task);
}
}
}
}
/**
* 🎯 TEILZIEL 1: addTask(title, priority = 'medium', dueDate = null)
*/
addTask(title, priority = "medium", dueDate = null) {
if (typeof title !== "string" || title.trim().length === 0) {
throw new Error("Aufgabentitel darf nicht leer sein");
}
const validPriority = ["high", "medium", "low"].includes(priority) ? priority : "medium";
const normalizedDueDate = (typeof dueDate === "string" && dueDate.trim().length > 0) ? dueDate.trim() : null;
const task = {
id: this.nextId++,
title: title.trim(),
priority: validPriority,
completed: false,
dueDate: normalizedDueDate,
createdAt: new Date().toISOString()
};
this.tasks.push(task);
return task;
}
/**
* 🎯 TEILZIEL 2: toggleTask(id)
*/
toggleTask(id) {
const numId = Number(id);
const task = this.tasks.find(t => t.id === numId);
if (!task) {
return null;
}
task.completed = !task.completed;
return task;
}
/**
* 🎯 TEILZIEL 3: deleteTask(id)
*/
deleteTask(id) {
const numId = Number(id);
const index = this.tasks.findIndex(t => t.id === numId);
if (index === -1) {
return false;
}
this.tasks.splice(index, 1);
return true;
}
/**
* 🎯 TEILZIEL 4: getFilteredTasks(filterStatus = 'all', searchQuery = '')
*/
getFilteredTasks(filterStatus = "all", searchQuery = "") {
let result = [...this.tasks];
if (filterStatus === "active") {
result = result.filter(t => !t.completed);
} else if (filterStatus === "completed") {
result = result.filter(t => t.completed);
}
if (typeof searchQuery === "string" && searchQuery.trim().length > 0) {
const query = searchQuery.trim().toLowerCase();
result = result.filter(t => t.title.toLowerCase().includes(query));
}
return result;
}
/**
* 🎯 TEILZIEL 5: getStatistics()
*/
getStatistics() {
const total = this.tasks.length;
const completed = this.tasks.filter(t => t.completed).length;
const active = total - completed;
const highPriorityCount = this.tasks.filter(t => t.priority === "high").length;
return {
total,
active,
completed,
highPriorityCount
};
}
/**
* Hilfsmethode: Task nach ID suchen
*/
getTaskById(id) {
const numId = Number(id);
return this.tasks.find(t => t.id === numId) || null;
}
/**
* Hilfsmethode: Alle erledigten Aufgaben auf einmal löschen
*/
clearCompleted() {
const initialCount = this.tasks.length;
this.tasks = this.tasks.filter(t => !t.completed);
return initialCount - this.tasks.length;
}
}

View file

@ -0,0 +1,111 @@
/**
* 🧪 Testsuite für Master 08: Interaktive Task-App (TaskManager)
*/
function assertThrows(fn, msg) {
let threw = false;
try {
fn();
} catch (e) {
threw = true;
}
assert.ok(threw, msg || "Erwarteter Fehler wurde nicht geworfen!");
}
// Instanz erzeugen
const manager = new TaskManager();
// ----------------------------------------------------
// 1. Tests für addTask(title, priority, dueDate)
// ----------------------------------------------------
const t1 = manager.addTask("TypeScript Typen & Interfaces lernen", "high", "2026-12-31");
assert.strictEqual(t1.id, 1, "Erste Aufgabe sollte ID 1 erhalten");
assert.strictEqual(t1.title, "TypeScript Typen & Interfaces lernen", "Titel korrekt");
assert.strictEqual(t1.priority, "high", "Priorität sollte 'high' sein");
assert.strictEqual(t1.completed, false, "Initialer Status muss completed: false sein");
assert.strictEqual(t1.dueDate, "2026-12-31", "Fälligkeitsdatum korrekt");
assert.ok(typeof t1.createdAt === "string", "createdAt muss gesetzt sein");
const t2 = manager.addTask(" CSS Design System verfeinern ");
assert.strictEqual(t2.id, 2, "Zweite Aufgabe sollte ID 2 erhalten");
assert.strictEqual(t2.title, "CSS Design System verfeinern", "Titel muss getrimmt sein");
assert.strictEqual(t2.priority, "medium", "Standard-Priorität sollte 'medium' sein");
assert.strictEqual(t2.dueDate, null, "Ohne Fälligkeitsdatum sollte null gesetzt werden");
const t3 = manager.addTask("Unit Tests für State-Manager schreiben", "low");
assert.strictEqual(t3.id, 3, "Dritte Aufgabe sollte ID 3 erhalten");
const t4 = manager.addTask("Master Release v1.0 bereitstellen", "high", "2026-09-01");
assert.strictEqual(t4.id, 4, "Vierte Aufgabe sollte ID 4 erhalten");
// Validierung von fehlerhaftem Input
assertThrows(() => manager.addTask(""), "Leerer Titel muss Error werfen");
assertThrows(() => manager.addTask(" "), "Whitespace-Titel muss Error werfen");
assertThrows(() => manager.addTask(null), "null als Titel muss Error werfen");
// ----------------------------------------------------
// 2. Tests für toggleTask(id)
// ----------------------------------------------------
const toggled1 = manager.toggleTask(1);
assert.strictEqual(toggled1.completed, true, "Task 1 sollte nach erstem Toggle completed: true sein");
const toggledAgain = manager.toggleTask(1);
assert.strictEqual(toggledAgain.completed, false, "Task 1 sollte nach zweitem Toggle wieder completed: false sein");
const toggledNonExistent = manager.toggleTask(999);
assert.strictEqual(toggledNonExistent, null, "Toggle für unbekannte ID sollte null liefern");
// Für nachfolgende Tests Task 2 auf completed setzen
manager.toggleTask(2);
// ----------------------------------------------------
// 3. Tests für deleteTask(id)
// ----------------------------------------------------
const deleted = manager.deleteTask(3);
assert.strictEqual(deleted, true, "deleteTask(3) sollte true zurückgeben");
assert.strictEqual(manager.tasks.length, 3, "Nach Löschen sollten noch 3 Tasks vorhanden sein");
const deletedAgain = manager.deleteTask(3);
assert.strictEqual(deletedAgain, false, "Erneutes Löschen derselben ID sollte false liefern");
// ----------------------------------------------------
// 4. Tests für getFilteredTasks(filterStatus, searchQuery)
// ----------------------------------------------------
// Aktueller Zustand:
// - Task 1: "TypeScript ...", high, completed: false
// - Task 2: "CSS Design ...", medium, completed: true
// - Task 4: "Master Release ...", high, completed: false
const allTasks = manager.getFilteredTasks("all");
assert.strictEqual(allTasks.length, 3, "Filter 'all' sollte alle 3 Tasks liefern");
const activeTasks = manager.getFilteredTasks("active");
assert.strictEqual(activeTasks.length, 2, "Filter 'active' sollte 2 Tasks liefern");
assert.strictEqual(activeTasks[0].id, 1, "Erster aktiver Task");
assert.strictEqual(activeTasks[1].id, 4, "Zweiter aktiver Task");
const completedTasks = manager.getFilteredTasks("completed");
assert.strictEqual(completedTasks.length, 1, "Filter 'completed' sollte 1 Task liefern");
assert.strictEqual(completedTasks[0].id, 2, "Erledigter Task ID");
// Volltextsuche
const searchResult = manager.getFilteredTasks("all", "release");
assert.strictEqual(searchResult.length, 1, "Suche nach 'release' sollte Task 4 finden");
assert.strictEqual(searchResult[0].id, 4, "Gefundene Task-ID");
const searchCaseInsensitive = manager.getFilteredTasks("all", "TYPESCRIPT");
assert.strictEqual(searchCaseInsensitive.length, 1, "Suche sollte case-insensitive sein");
const searchEmpty = manager.getFilteredTasks("all", "NICHT_VORHANDEN_123");
assert.strictEqual(searchEmpty.length, 0, "Erfolglose Suche liefert leeres Array");
// ----------------------------------------------------
// 5. Tests für getStatistics()
// ----------------------------------------------------
const stats = manager.getStatistics();
assert.strictEqual(stats.total, 3, "Gesamtzahl Tasks");
assert.strictEqual(stats.active, 2, "Aktive Tasks");
assert.strictEqual(stats.completed, 1, "Erledigte Tasks");
assert.strictEqual(stats.highPriorityCount, 2, "Hohe Priorität Tasks (Task 1 & Task 4)");
console.log("✅ Alle Tests für Master 08 (Interaktive Task-App / TaskManager) erfolgreich bestanden!");

View file

@ -4,16 +4,38 @@
"language": "javascript",
"icon": "🌐",
"runner": "javascript",
"description": "Vom Browser-Scripting über ES6-Funktionen, Arrays bis hin zu asynchronem JavaScript.",
"description": "Vom Browser-Scripting über ES6-Funktionen, Arrays bis hin zu TypeScript Type Safety und dem Master-Abschlussprojekt.",
"tracks": [
{
"id": "track_1_grundlagen",
"title": "🌱 Lehrpfad 1: JavaScript Grundlagen & ES6",
"description": "Variablen (let/const), Template Strings, Arrow Functions und Array-Methoden.",
"description": "Variablen, Template Strings, Arrow Functions, Arrays, Objekte, Destructuring und DOM-Manipulation & Events.",
"certificateKey": "js_grundlagen",
"chapters": [
{ "folder": "01_erste_schritte_variablen", "title": "JS 01: Variablen, Typen & Template-Strings", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
{ "folder": "02_funktionen_und_arrays", "title": "JS 02: Arrow Functions, Arrays & Map/Filter", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
{ "folder": "02_funktionen_und_arrays", "title": "JS 02: Arrow Functions, Arrays & Map/Filter", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
{ "folder": "03_objekte_und_destructuring", "title": "JS 03: Objekte, Destructuring & Rest/Spread", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
{ "folder": "04_dom_und_events", "title": "JS 04: DOM-Manipulation & Events", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
]
},
{
"id": "track_2_async_und_apis",
"title": "⚡ Lehrpfad 2: Asynchrones JavaScript, Promises & REST-APIs",
"description": "Callbacks, Event Loop, Promises, Chaining, Promise.all, Async/Await und REST-APIs mit JSON.",
"certificateKey": "js_async",
"chapters": [
{ "folder": "05_callbacks_und_promises", "title": "JS 05: Callbacks, Event Loop & Promises", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
{ "folder": "06_async_await_und_fetch", "title": "JS 06: Async/Await & REST-APIs (Fetch)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
]
},
{
"id": "track_3_typescript_und_master",
"title": "🛡️ Lehrpfad 3: TypeScript & Master-Abschlussprojekt",
"description": "Statische Typsicherheit, Interfaces, Generics, State-Management und vollwertige Web-Applikation.",
"certificateKey": "js_master",
"chapters": [
{ "folder": "07_einstieg_typescript", "title": "TS 07: TypeScript Type Safety & Interfaces", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
{ "folder": "08_abschlussprojekt_web_app", "title": "Master 08: Interaktive Task-App (MVC)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
]
}
]