Vollständiges JavaScript & TypeScript Curriculum (8 Module über 3 Lehrpfade: ES6 Grundlagen, Asynchrones JS & Fetch, TypeScript & Web-App Abschlussprojekt)
This commit is contained in:
parent
aff02ca52e
commit
1916a634f4
38 changed files with 3799 additions and 14 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -72,8 +72,19 @@
|
|||
|
||||
try {
|
||||
const runFn = new Function(e.data.code);
|
||||
runFn();
|
||||
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
|
||||
return (async () => {
|
||||
${testCode}
|
||||
|
||||
({ total, passed, failures });
|
||||
})();
|
||||
`;
|
||||
|
||||
this.runCode(fullScript, logCallback).then((res) => {
|
||||
|
|
|
|||
|
|
@ -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 →</a>
|
||||
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">Weiter zu JS 02 →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -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 →</a>
|
||||
<a href="../01_erste_schritte_variablen/index.html" class="btn btn-secondary">← Zurück zu JS 01</a>
|
||||
<a href="../03_objekte_und_destructuring/index.html" class="btn btn-secondary">Weiter zu JS 03 →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
|||
118
courses/javascript/03_objekte_und_destructuring/README.md
Normal file
118
courses/javascript/03_objekte_und_destructuring/README.md
Normal 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!
|
||||
46
courses/javascript/03_objekte_und_destructuring/aufgabe.js
Normal file
46
courses/javascript/03_objekte_und_destructuring/aufgabe.js
Normal 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;
|
||||
}
|
||||
171
courses/javascript/03_objekte_und_destructuring/index.html
Normal file
171
courses/javascript/03_objekte_und_destructuring/index.html
Normal 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 & 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 & 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 & 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- & 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 & Auspack-Station</div>
|
||||
<p>Ein Objekt ist wie eine beschriftete <strong>Werkzeugkiste</strong>. Destructuring ist das gezielte Herausgreifen von genau dem Werkzeug (z. 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 & 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 & 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 & 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 }) => 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 →</a>
|
||||
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">← Zurück zu JS 02</a>
|
||||
<a href="../04_dom_und_events/index.html" class="btn btn-secondary">Weiter zu JS 04 →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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!");
|
||||
143
courses/javascript/04_dom_und_events/README.md
Normal file
143
courses/javascript/04_dom_und_events/README.md
Normal 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!
|
||||
48
courses/javascript/04_dom_und_events/aufgabe.js
Normal file
48
courses/javascript/04_dom_und_events/aufgabe.js
Normal 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;
|
||||
}
|
||||
164
courses/javascript/04_dom_und_events/index.html
Normal file
164
courses/javascript/04_dom_und_events/index.html
Normal 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 & 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 & Web APIs</div>
|
||||
<h1>JS 04: DOM-Manipulation & 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 & 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>
|
||||
└── <html><br>
|
||||
├── <head> (title, meta, link)<br>
|
||||
└── <body><br>
|
||||
├── <h1 id="titel"><br>
|
||||
├── <button class="btn"><br>
|
||||
└── <ul id="aufgaben"> ── <li class="erledigt">
|
||||
</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 & 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 & 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 & 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> & <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 & 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 →</a>
|
||||
<a href="../03_objekte_und_destructuring/index.html" class="btn btn-secondary">← Zurück zu JS 03</a>
|
||||
<a href="../05_callbacks_und_promises/index.html" class="btn btn-secondary">Weiter zu JS 05 →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
40
courses/javascript/04_dom_und_events/musterloesung.js
Normal file
40
courses/javascript/04_dom_und_events/musterloesung.js
Normal 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"
|
||||
};
|
||||
}
|
||||
127
courses/javascript/04_dom_und_events/test_aufgabe.js
Normal file
127
courses/javascript/04_dom_und_events/test_aufgabe.js
Normal 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!");
|
||||
131
courses/javascript/05_callbacks_und_promises/README.md
Normal file
131
courses/javascript/05_callbacks_und_promises/README.md
Normal 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.
|
||||
44
courses/javascript/05_callbacks_und_promises/aufgabe.js
Normal file
44
courses/javascript/05_callbacks_und_promises/aufgabe.js
Normal 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;
|
||||
}
|
||||
203
courses/javascript/05_callbacks_und_promises/index.html
Normal file
203
courses/javascript/05_callbacks_und_promises/index.html
Normal 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 & 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 & 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 & Event Loop</h2>
|
||||
<p>
|
||||
JavaScript ist <strong>Single-Threaded</strong>. Wenn eine Aufgabe Zeit benötigt (z. 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> → <code>fulfilled</code></strong>). Brennt die Küche, blinkt er rot (<strong><code>reject</code> → <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">→</span>
|
||||
<span style="font-size: 0.85rem; font-weight: 700; color: #166534;">resolve(wert)</span>
|
||||
<span class="diagram-arrow">→</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">→</span>
|
||||
<span style="font-size: 0.85rem; font-weight: 700; color: #9f1239;">reject(error)</span>
|
||||
<span class="diagram-arrow">→</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 & 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 & 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> & <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 > 0 bzw. <code>reject</code> bei <= 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 & 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 →</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 →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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]);
|
||||
}
|
||||
78
courses/javascript/05_callbacks_und_promises/test_aufgabe.js
Normal file
78
courses/javascript/05_callbacks_und_promises/test_aufgabe.js
Normal 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!");
|
||||
142
courses/javascript/06_async_await_und_fetch/README.md
Normal file
142
courses/javascript/06_async_await_und_fetch/README.md
Normal 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.
|
||||
42
courses/javascript/06_async_await_und_fetch/aufgabe.js
Normal file
42
courses/javascript/06_async_await_und_fetch/aufgabe.js
Normal 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;
|
||||
}
|
||||
189
courses/javascript/06_async_await_und_fetch/index.html
Normal file
189
courses/javascript/06_async_await_und_fetch/index.html
Normal 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 & 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 & 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 & .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 & 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 & 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 & 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;">← 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 →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
50
courses/javascript/06_async_await_und_fetch/musterloesung.js
Normal file
50
courses/javascript/06_async_await_und_fetch/musterloesung.js
Normal 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;
|
||||
}
|
||||
}
|
||||
138
courses/javascript/06_async_await_und_fetch/test_aufgabe.js
Normal file
138
courses/javascript/06_async_await_und_fetch/test_aufgabe.js
Normal 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!");
|
||||
151
courses/javascript/07_einstieg_typescript/README.md
Normal file
151
courses/javascript/07_einstieg_typescript/README.md
Normal 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.
|
||||
122
courses/javascript/07_einstieg_typescript/aufgabe.js
Normal file
122
courses/javascript/07_einstieg_typescript/aufgabe.js
Normal 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 "";
|
||||
}
|
||||
175
courses/javascript/07_einstieg_typescript/index.html
Normal file
175
courses/javascript/07_einstieg_typescript/index.html
Normal 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 & 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 & Type Safety</div>
|
||||
<h1>TS 07: TypeScript Type Safety & 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 & 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 & 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 & 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 & Positionen</div>
|
||||
<div style="border-left: 2px solid #38bdf8; padding-left: 12px; margin-bottom: 12px;">
|
||||
<span style="color: #fbbf24;">interface Auftrag</span> {<br>
|
||||
id: <span style="color: #a78bfa;">string | number</span>;<br>
|
||||
kundeId: <span style="color: #a78bfa;">string | number</span>;<br>
|
||||
positionen: <span style="color: #34d399;">Auftragsposition[]</span>;<br>
|
||||
summe: <span style="color: #a78bfa;">number</span>;<br>
|
||||
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>
|
||||
titel: <span style="color: #a78bfa;">string</span>;<br>
|
||||
menge: <span style="color: #a78bfa;">number</span>;<br>
|
||||
einzelpreis: <span style="color: #a78bfa;">number</span>;<br>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box box-tipp">
|
||||
💡 <strong>Optional Chaining & 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><T></code>) schreibst du Funktionen, die für beliebige Typen funktionieren, ohne den Typenverlust von <code>any</code>:
|
||||
</p>
|
||||
<pre><code>function umkehren<T>(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 & 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 & Tests starten →</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">← 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 →</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../../../assets/ui_enhancements.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
132
courses/javascript/07_einstieg_typescript/musterloesung.js
Normal file
132
courses/javascript/07_einstieg_typescript/musterloesung.js
Normal 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()}`;
|
||||
}
|
||||
105
courses/javascript/07_einstieg_typescript/test_aufgabe.js
Normal file
105
courses/javascript/07_einstieg_typescript/test_aufgabe.js
Normal 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!");
|
||||
112
courses/javascript/08_abschlussprojekt_web_app/README.md
Normal file
112
courses/javascript/08_abschlussprojekt_web_app/README.md
Normal 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!
|
||||
295
courses/javascript/08_abschlussprojekt_web_app/app.js
Normal file
295
courses/javascript/08_abschlussprojekt_web_app/app.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
145
courses/javascript/08_abschlussprojekt_web_app/aufgabe.js
Normal file
145
courses/javascript/08_abschlussprojekt_web_app/aufgabe.js
Normal 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;
|
||||
}
|
||||
}
|
||||
179
courses/javascript/08_abschlussprojekt_web_app/index.html
Normal file
179
courses/javascript/08_abschlussprojekt_web_app/index.html
Normal 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 & 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+ & 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 & 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 & 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 & 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 & 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 & 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 & Unittests starten →</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">← Zurück zu Modul 07</a>
|
||||
<a href="../../../index.html" class="btn" style="background: var(--success); color: white;">🏆 Kurs abgeschlossen – Zur Übersicht →</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>
|
||||
154
courses/javascript/08_abschlussprojekt_web_app/musterloesung.js
Normal file
154
courses/javascript/08_abschlussprojekt_web_app/musterloesung.js
Normal 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;
|
||||
}
|
||||
}
|
||||
111
courses/javascript/08_abschlussprojekt_web_app/test_aufgabe.js
Normal file
111
courses/javascript/08_abschlussprojekt_web_app/test_aufgabe.js
Normal 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!");
|
||||
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue