JavaScript-Grundlagen II

In dieser Einheit vertiefen wir unsere JavaScript-Kenntnisse und lernen, mit echten Daten zu arbeiten. Wir laden Daten aus JSON-Dateien und APIs, verarbeiten sie mit Array-Methoden und stellen sie dynamisch auf Webseiten dar. Der Fokus liegt auf Techniken, die in den Digital Humanities täglich gebraucht werden: Sammlungen durchsuchen, Metadaten filtern und Kulturerbe-Daten visualisieren.

1. Array-Methoden im Detail

In Einheit 05 haben wir Arrays kennengelernt — geordnete Listen von Werten. Jetzt lernen wir die Methoden kennen, mit denen wir Arrays effizient verarbeiten. Diese Methoden sind das Herzstück der Datenverarbeitung in JavaScript.

forEach() — Über alle Elemente iterieren

forEach() ruft eine Funktion für jedes Element im Array auf. Es ist die einfachste Methode, um etwas mit jedem Element zu tun:

const schlagwoerter = ['Romanistik', 'Kreolsprachen', 'Baskisch', 'Sprachkontakt'];

// Jedes Schlagwort in der Konsole ausgeben
schlagwoerter.forEach((wort) => {
  console.log(`Schlagwort: ${wort}`);
});

// Mit Index (zweiter Parameter)
schlagwoerter.forEach((wort, index) => {
  console.log(`${index + 1}. ${wort}`);
});
// 1. Romanistik
// 2. Kreolsprachen
// 3. Baskisch
// 4. Sprachkontakt

Hinweis

forEach() gibt keinen Wert zurück (undefined). Wenn Sie ein neues Array erzeugen wollen, verwenden Sie map().

map() — Jedes Element transformieren

map() erstellt ein neues Array, indem es eine Funktion auf jedes Element anwendet. Das Original-Array bleibt unverändert.

const briefe = [
  { titel: 'Brief an Ascoli', datum: '1882-01-15' },
  { titel: 'Brief an Meyer-Lübke', datum: '1887-03-12' },
  { titel: 'Brief an Vossler', datum: '1904-01-03' }
];

// Nur die Titel extrahieren
const titel = briefe.map(brief => brief.titel);
// ['Brief an Ascoli', 'Brief an Meyer-Lübke', 'Brief an Vossler']

// HTML-Karten erzeugen
const karten = briefe.map(brief => `
  <div class="karte">
    <h3>${brief.titel}</h3>
    <p>Datum: ${brief.datum}</p>
  </div>
`);

// In einen Container einfuegen
document.querySelector('#liste').innerHTML = karten.join('');

Schlüsselkonzept: map() für Daten-zu-HTML

Das Muster daten.map(item => HTML-String).join('') ist die Standardtechnik, um JavaScript-Daten in HTML-Elemente umzuwandeln. Sie werden es in fast jedem Projekt verwenden.

filter() — Elemente nach Bedingung auswählen

filter() erstellt ein neues Array mit allen Elementen, die eine bestimmte Bedingung erfüllen. Die Callback-Funktion muss true oder false zurückgeben:

const korrespondenz = [
  { absender: 'Schuchardt', empfaenger: 'Ascoli', jahr: 1882 },
  { absender: 'Meyer', empfaenger: 'Schuchardt', jahr: 1885 },
  { absender: 'Schuchardt', empfaenger: 'Baudouin', jahr: 1890 },
  { absender: 'Schuchardt', empfaenger: 'Vossler', jahr: 1904 },
  { absender: 'Meringer', empfaenger: 'Schuchardt', jahr: 1907 }
];

// Briefe von Schuchardt
const vonSchuchardt = korrespondenz.filter(brief =>
  brief.absender === 'Schuchardt'
);
// 3 Ergebnisse

// Briefe nach 1890
const nach1890 = korrespondenz.filter(brief => brief.jahr > 1890);
// 2 Ergebnisse

// Verkettung: Briefe von Schuchardt nach 1890
const schuchardtNach1890 = korrespondenz
  .filter(brief => brief.absender === 'Schuchardt')
  .filter(brief => brief.jahr > 1890);
// 1 Ergebnis: Brief an Vossler

find() — Das erste passende Element

find() funktioniert wie filter(), gibt aber nur das erste passende Element zurück (nicht ein Array):

const objekte = [
  { id: 'KM-001', name: 'Richtbeil', kategorie: 'Strafvollzug' },
  { id: 'KM-002', name: 'Schandmaske', kategorie: 'Ehrenstrafe' },
  { id: 'KM-003', name: 'Folterzange', kategorie: 'Folter' }
];

// Objekt mit bestimmter ID finden
const objekt = objekte.find(o => o.id === 'KM-002');
console.log(objekt.name); // 'Schandmaske'

// Wenn nichts gefunden wird: undefined
const fehlt = objekte.find(o => o.id === 'KM-999');
console.log(fehlt); // undefined

// Darum immer pruefen:
if (fehlt) {
  console.log(fehlt.name);
} else {
  console.log('Objekt nicht gefunden');
}

reduce() — Auf einen einzelnen Wert reduzieren

reduce() verarbeitet alle Elemente und fasst sie zu einem einzigen Wert zusammen. Es ist die mächtigste, aber auch komplexeste Array-Methode:

const briefeProJahr = [
  { jahr: 1882, anzahl: 12 },
  { jahr: 1885, anzahl: 8 },
  { jahr: 1890, anzahl: 15 },
  { jahr: 1904, anzahl: 5 }
];

// Gesamtzahl berechnen
const gesamt = briefeProJahr.reduce((summe, eintrag) => {
  return summe + eintrag.anzahl;
}, 0);
console.log(gesamt); // 40

// Briefe nach Dekade gruppieren
const nachDekade = korrespondenz.reduce((gruppen, brief) => {
  const dekade = Math.floor(brief.jahr / 10) * 10;
  const key = `${dekade}er`;
  if (!gruppen[key]) {
    gruppen[key] = [];
  }
  gruppen[key].push(brief);
  return gruppen;
}, {});
// { '1880er': [...], '1890er': [...], '1900er': [...] }

sort() — Elemente sortieren

sort() sortiert ein Array an Ort und Stelle (verändert das Original!):

// Strings werden alphabetisch sortiert
const sprachen = ['Baskisch', 'Romanisch', 'Albanisch', 'Kreolisch'];
sprachen.sort();
// ['Albanisch', 'Baskisch', 'Kreolisch', 'Romanisch']

// Zahlen brauchen eine Vergleichsfunktion!
const jahre = [1904, 1882, 1890, 1885];
jahre.sort((a, b) => a - b); // aufsteigend
// [1882, 1885, 1890, 1904]
jahre.sort((a, b) => b - a); // absteigend
// [1904, 1890, 1885, 1882]

// Objekte nach Eigenschaft sortieren
const briefeListe = [
  { titel: 'Brief an Vossler', jahr: 1904 },
  { titel: 'Brief an Ascoli', jahr: 1882 },
  { titel: 'Brief an Baudouin', jahr: 1890 }
];
briefeListe.sort((a, b) => a.jahr - b.jahr);
// Sortiert nach Jahr: 1882, 1890, 1904

Achtung

sort() verändert das Original-Array! Wenn Sie das Original behalten wollen, erstellen Sie zuerst eine Kopie: [...array].sort()

Ausprobieren

Erstellen Sie ein Array mit 5 Objekten (z.B. Musemsobjekte mit Name, Jahr und Kategorie). Verwenden Sie filter(), um alle Objekte einer bestimmten Kategorie zu finden, sortieren Sie das Ergebnis nach Jahr und geben Sie die Namen mit map() aus.

2. Objekte und Destrukturierung

Objekte sind neben Arrays die wichtigste Datenstruktur in JavaScript. In den Digital Humanities begegnen uns Objekte überall: Metadaten, API-Responses, Konfigurationen.

Verschachtelte Objekte

Reale Daten sind fast immer verschachtelt:

const manuskript = {
  id: 'MS-042',
  titel: 'Kreolische Studien I',
  autor: {
    name: 'Hugo Schuchardt',
    geburtsjahr: 1842,
    wirkungsort: {
      stadt: 'Graz',
      institution: 'Karl-Franzens-Universität'
    }
  },
  publikation: {
    jahr: 1882,
    zeitschrift: 'Sitzungsberichte der kaiserlichen Akademie der Wissenschaften',
    band: 101
  },
  schlagwoerter: ['Kreolsprachen', 'Sprachkontakt', 'Portugiesisch']
};

// Zugriff auf verschachtelte Werte
console.log(manuskript.autor.name);
// 'Hugo Schuchardt'

console.log(manuskript.autor.wirkungsort.stadt);
// 'Graz'

console.log(manuskript.schlagwoerter[1]);
// 'Sprachkontakt'

Destrukturierung von Objekten

Destrukturierung extrahiert Eigenschaften aus einem Objekt direkt in Variablen. Das macht den Code kürzer und lesbarer:

const brief = {
  titel: 'Brief an Karl Vossler',
  datum: '1904-01-03',
  absender: 'Hugo Schuchardt',
  ort: 'Graz',
  seiten: 4
};

// Destrukturierung: Eigenschaften direkt in Variablen
const { titel, datum, absender } = brief;
console.log(titel);    // 'Brief an Karl Vossler'
console.log(datum);    // '1904-01-03'
console.log(absender); // 'Hugo Schuchardt'

// Mit Umbenennung
const { titel: briefTitel, ort: versandOrt } = brief;
console.log(briefTitel); // 'Brief an Karl Vossler'
console.log(versandOrt); // 'Graz'

// Mit Standardwert
const { sprache = 'Deutsch' } = brief;
console.log(sprache); // 'Deutsch' (nicht im Objekt, also Standardwert)

// Verschachtelte Destrukturierung
const { autor: { name, wirkungsort: { stadt } } } = manuskript;
console.log(name);  // 'Hugo Schuchardt'
console.log(stadt); // 'Graz'

Spread-Operator (...)

Der Spread-Operator kopiert Eigenschaften von einem Objekt in ein anderes oder erstellt Kopien:

// Objekt kopieren
const original = { name: 'Richtbeil', jahr: 1798 };
const kopie = { ...original };

// Objekt erweitern
const erweitert = { ...original, kategorie: 'Strafvollzug', museum: 'Kriminalmuseum' };
// { name: 'Richtbeil', jahr: 1798, kategorie: 'Strafvollzug', museum: 'Kriminalmuseum' }

// Eigenschaft ueberschreiben
const aktualisiert = { ...original, jahr: 1800 };
// { name: 'Richtbeil', jahr: 1800 }

// Array kopieren und erweitern
const alt = ['Romanisch', 'Baskisch'];
const neu = [...alt, 'Keltisch', 'Kreolisch'];
// ['Romanisch', 'Baskisch', 'Keltisch', 'Kreolisch']

Object.keys(), Object.values(), Object.entries()

const metadaten = {
  titel: 'Brief an Ascoli',
  datum: '1882-01-15',
  sprache: 'Deutsch',
  seiten: 3
};

// Alle Schluessel als Array
console.log(Object.keys(metadaten));
// ['titel', 'datum', 'sprache', 'seiten']

// Alle Werte als Array
console.log(Object.values(metadaten));
// ['Brief an Ascoli', '1882-01-15', 'Deutsch', 3]

// Schluessel-Wert-Paare als Array von Arrays
console.log(Object.entries(metadaten));
// [['titel', 'Brief an Ascoli'], ['datum', '1882-01-15'], ...]

// Praktisch: Metadaten als HTML-Tabelle
const tabellenZeilen = Object.entries(metadaten)
  .map(([key, value]) => `<tr><td>${key}</td><td>${value}</td></tr>`)
  .join('');
const tabelle = `<table>${tabellenZeilen}</table>`;

3. Asynchroner Code

Bisher haben wir Code geschrieben, der Zeile für Zeile ausgeführt wird (synchron). Beim Laden von Daten ändert sich das: Die Daten sind nicht sofort da, und JavaScript muss warten können, ohne die gesamte Seite zu blockieren.

Warum asynchron?

// Stellen Sie sich vor, das wuerde blockieren:
const daten = fetch('https://api.europeana.eu/...'); // 2 Sekunden warten
// Währenddessen: Seite eingefroren, keine Klicks moeglich!

// Stattdessen: Asynchron weiterarbeiten
fetch('https://api.europeana.eu/...')
  .then(response => {
    // Wird ausgefuehrt, WENN die Daten ankommen
    console.log('Daten sind da!');
  });
console.log('Dieser Code läuft SOFORT weiter!');

Schlüsselkonzept: Asynchronität

JavaScript führt Code nicht blockierend aus. Wenn eine Operation Zeit braucht (Daten laden, Timer warten), wird sie im Hintergrund ausgeführt. Sobald das Ergebnis da ist, wird eine Callback-Funktion aufgerufen. So bleibt die Seite währenddessen reaktionsfähig.

Das Callback-Problem

Früher verwendete man verschachtelte Callbacks. Das führte zur sogenannten „Callback Hell“:

// Callback Hell (so bitte NICHT mehr programmieren!)
ladeDaten(url, function(daten) {
  verarbeiteDaten(daten, function(ergebnis) {
    ladeMehr(ergebnis.nextUrl, function(mehrDaten) {
      zeigeDaten(mehrDaten, function() {
        console.log('Fertig!');
        // Immer tiefer verschachtelt...
      });
    });
  });
});

Promises: Die Lösung

Ein Promise repräsentiert einen Wert, der jetzt oder in der Zukunft verfügbar sein wird. Er hat drei Zustände:

// Ein Promise mit .then() und .catch()
fetch('briefe.json')
  .then(response => response.json())     // fulfilled: Response parsen
  .then(data => console.log(data))       // fulfilled: Daten verwenden
  .catch(error => console.error(error)); // rejected: Fehler behandeln

async/await: Die moderne Lösung

async/await ist syntaktischer Zucker für Promises. Es lässt asynchronen Code aussehen wie synchronen Code:

// Die async-Funktion gibt immer ein Promise zurueck
async function ladeBriefe() {
  // await pausiert die Funktion, bis das Promise erfuellt ist
  const response = await fetch('briefe.json');
  const daten = await response.json();
  return daten;
}

// Aufruf (auch mit await oder .then)
const briefe = await ladeBriefe();
// oder
ladeBriefe().then(briefe => console.log(briefe));

Tipp

await kann nur innerhalb einer async-Funktion verwendet werden. Auf oberster Ebene (außerhalb einer Funktion) funktioniert es nur in Modulen (<script type="module">).

4. Die Fetch API

fetch() ist die moderne Browser-API zum Laden von Daten. Sie ersetzt das ältere XMLHttpRequest und ist deutlich einfacher zu verwenden.

Grundsyntax

// Einfachster Aufruf
const response = await fetch('daten.json');

// Mit vollstaendiger URL
const response2 = await fetch('https://api.example.com/items');

// Mit Optionen
const response3 = await fetch('https://api.example.com/items', {
  method: 'GET',      // HTTP-Methode (GET ist Standard)
  headers: {
    'Accept': 'application/json'
  }
});

Das Response-Objekt

fetch() gibt ein Response-Objekt zurück mit wichtigen Eigenschaften:

const response = await fetch('briefe.json');

// Status pruefen
console.log(response.ok);     // true (Status 200-299)
console.log(response.status); // 200
console.log(response.statusText); // 'OK'

// Body auslesen (nur einmal moeglich!)
const jsonDaten = await response.json();  // als JSON
// ODER
const textDaten = await response.text();  // als Text
// ODER
const blobDaten = await response.blob();  // als Binaerdaten

Achtung

Der Body kann nur einmal ausgelesen werden! Wenn Sie .json() aufrufen, können Sie danach nicht mehr .text() aufrufen. Speichern Sie das Ergebnis in einer Variable.

Fehlerbehandlung mit try/catch

async function ladeDaten(url) {
  try {
    const response = await fetch(url);

    // fetch wirft bei HTTP-Fehlern KEINEN Error!
    // Wir muessen selbst pruefen:
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const daten = await response.json();
    return daten;

  } catch (error) {
    // Netzwerkfehler ODER unser geworfener Fehler
    console.error('Fehler beim Laden:', error.message);
    return null;
  }
}

// Verwendung
const briefe = await ladeDaten('briefe.json');
if (briefe) {
  // Daten verwenden
  console.log(`${briefe.length} Briefe geladen`);
} else {
  // Fehlerfall
  console.log('Keine Daten verfuegbar');
}

Schlüsselkonzept: fetch wirft nicht bei HTTP-Fehlern

fetch() wirft nur bei Netzwerkfehlern einen Error (z.B. kein Internet). Ein HTTP 404 (nicht gefunden) oder 500 (Serverfehler) wird nicht als Fehler behandelt — Sie müssen response.ok selbst prüfen!

Ausprobieren

Erstellen Sie eine JSON-Datei mit 3-5 Museumsobjekten. Laden Sie sie mit fetch() und zeigen Sie die Objekte als Karten auf einer HTML-Seite an. Verwenden Sie try/catch für die Fehlerbehandlung.

5. JSON verstehen und verarbeiten

JSON (JavaScript Object Notation) ist das Standardformat für den Datenaustausch im Web. Fast jede API liefert Daten im JSON-Format.

JSON-Format

JSON ist ein Textformat, das auf der JavaScript-Objektsyntax basiert:

{
  "id": "KM-001",
  "name": "Richtbeil",
  "datierung": "1798",
  "kategorie": "Strafvollzug",
  "beschreibung": "Instrument des Strafvollzugs aus dem 18. Jahrhundert.",
  "masse": {
    "laenge_cm": 45,
    "gewicht_kg": 2.3
  },
  "schlagwoerter": ["Strafvollzug", "18. Jahrhundert", "Justiz"],
  "ausgestellt": true,
  "bild_url": null
}

Erlaubte Datentypen in JSON:

JSON vs. JavaScript-Objekte

JSON sieht aus wie ein JavaScript-Objekt, hat aber strengere Regeln: Schlüssel müssen in doppelten Anführungszeichen stehen, keine Kommentare erlaubt, keine Funktionen möglich, kein trailing comma.

JSON parsen und erzeugen

// JSON-String in JavaScript-Objekt umwandeln
const jsonString = '{"name": "Schandmaske", "jahr": 1650}';
const objekt = JSON.parse(jsonString);
console.log(objekt.name); // 'Schandmaske'

// JavaScript-Objekt in JSON-String umwandeln
const daten = { name: 'Folterzange', jahr: 1720 };
const json = JSON.stringify(daten);
console.log(json); // '{"name":"Folterzange","jahr":1720}'

// Huebsch formatiert (fuer Debugging)
const huebsch = JSON.stringify(daten, null, 2);
console.log(huebsch);
// {
//   "name": "Folterzange",
//   "jahr": 1720
// }

// Bei fetch: response.json() kombiniert Text-Lesen und Parsen
const response = await fetch('objekte.json');
const objekte = await response.json(); // = JSON.parse(await response.text())

Verschachtelter Zugriff auf JSON-Daten

const europeanaResponse = {
  success: true,
  totalResults: 245,
  items: [
    {
      id: '/12345/object_ABC',
      title: ['Brief von Hugo Schuchardt'],
      edmPreview: ['https://example.com/thumb.jpg'],
      year: ['1887'],
      dataProvider: ['Universität Graz'],
      dcDescription: ['Ein handgeschriebener Brief...']
    }
  ]
};

// Auf verschachtelte Daten zugreifen
const erstesItem = europeanaResponse.items[0];
const titel = erstesItem.title[0];
const jahr = erstesItem.year?.[0] || 'Unbekannt'; // Optional Chaining

// Alle Titel extrahieren
const alleTitel = europeanaResponse.items.map(item =>
  item.title[0]
);

Tipp: Optional Chaining (?.)

Der Operator ?. verhindert Fehler bei fehlenden Werten: item.year?.[0] gibt undefined zurück statt einen Fehler zu werfen, wenn year nicht existiert.

6. APIs für die Digital Humanities

Was ist eine REST-API?

Eine REST-API (Representational State Transfer) ist eine Schnittstelle, die es erlaubt, Daten von einem Server abzurufen. Der Client (Ihr Browser/JavaScript) sendet eine HTTP-Anfrage an eine URL (den Endpunkt), und der Server antwortet mit Daten (meist JSON).

// Eine typische API-Anfrage
// Endpunkt + Query-Parameter
const url = 'https://api.europeana.eu/record/v2/search.json'
  + '?query=Schuchardt'    // Suchbegriff
  + '&rows=10'             // Max. 10 Ergebnisse
  + '&qf=TYPE:IMAGE'       // Nur Bilder
  + '&wskey=MEIN_KEY';     // API-Schluessel

const response = await fetch(url);
const data = await response.json();
console.log(`${data.totalResults} Ergebnisse gefunden`);

Europeana API im Detail

Europeana ist das größte Kulturerbe-Portal Europas. Die Europeana Search API ermöglicht den Zugriff auf über 50 Millionen Objekte aus Museen, Bibliotheken und Archiven.

Wichtige Suchparameter:

ParameterBeschreibungBeispiel
querySuchbegriffSchuchardt
qfFacettenfilterTYPE:IMAGE
rowsAnzahl Ergebnisse20 (max. 100)
startOffset (Paginierung)21
profileDetailgradstandard, rich
wskeyAPI-SchlüsselDEIN_KEY

Verfügbare Filter (qf):

IIIF: International Image Interoperability Framework

IIIF ist ein offener Standard, der von Bibliotheken, Museen und Archiven weltweit verwendet wird. Er definiert, wie digitalisierte Bilder über das Web bereitgestellt und angezeigt werden.

Zwei zentrale APIs:

IIIF Image API: URL-Aufbau

// Schema:
{server}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}

// Ganzes Bild, volle Groesse
https://iiif.example.org/img42/full/max/0/default.jpg

// Ausschnitt: x=100, y=200, Breite=500, Hoehe=300
https://iiif.example.org/img42/100,200,500,300/max/0/default.jpg

// Skaliert auf 800px Breite
https://iiif.example.org/img42/full/800,/0/default.jpg

// 90 Grad gedreht
https://iiif.example.org/img42/full/max/90/default.jpg

IIIF Manifeste: Struktur

Ein Manifest beschreibt ein ganzes Objekt. Es enthält Metadaten und eine Liste von Canvases (Seiten/Bilder):

// Vereinfachte Manifest-Struktur (IIIF Presentation API 3.0)
{
  "@context": "http://iiif.io/api/presentation/3/context.json",
  "id": "https://example.org/manifest/ms42",
  "type": "Manifest",
  "label": { "de": ["Handschrift MS 42"] },
  "metadata": [
    { "label": { "de": ["Autor"] }, "value": { "de": ["Hugo Schuchardt"] } },
    { "label": { "de": ["Datum"] }, "value": { "de": ["1882"] } }
  ],
  "items": [
    {
      "type": "Canvas",
      "label": { "de": ["Seite 1"] },
      "height": 4000,
      "width": 3000,
      "items": [{
        "type": "AnnotationPage",
        "items": [{
          "type": "Annotation",
          "motivation": "painting",
          "body": {
            "type": "Image",
            "id": "https://example.org/iiif/ms42-p1/full/max/0/default.jpg",
            "format": "image/jpeg",
            "height": 4000,
            "width": 3000
          }
        }]
      }]
    }
  ]
}

Ausprobieren

Öffnen Sie ein öffentliches IIIF-Manifest in Ihrem Browser (z.B. von der Bayerischen Staatsbibliothek oder der British Library). Versuchen Sie, die Struktur zu verstehen: Wo sind die Bilder-URLs? Wo die Beschriftungen?

7. Daten filtern und sortieren

Eine der häufigsten Aufgaben in DH-Projekten: Daten laden, anzeigen und dem Benutzer ermöglichen, sie zu durchsuchen und zu filtern. Hier ist das vollständige Muster.

Live-Suche: Das Standardmuster

<!-- HTML-Struktur -->
<input type="text" id="suche" placeholder="Suchen...">
<select id="filter-kategorie">
  <option value="">Alle Kategorien</option>
  <option value="Strafvollzug">Strafvollzug</option>
  <option value="Ehrenstrafe">Ehrenstrafe</option>
  <option value="Folter">Folter</option>
</select>
<div id="ergebnisse"></div>
// JavaScript: Live-Suche und Filter
let alleDaten = []; // Wird nach dem Laden befuellt

// Render-Funktion: Daten als HTML darstellen
function zeigeErgebnisse(daten) {
  const container = document.querySelector('#ergebnisse');

  if (daten.length === 0) {
    container.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
    return;
  }

  container.innerHTML = daten.map(obj => `
    <div class="karte">
      <h3>${obj.name}</h3>
      <p><strong>Kategorie:</strong> ${obj.kategorie}</p>
      <p><strong>Datierung:</strong> ${obj.datierung}</p>
      <p>${obj.beschreibung}</p>
    </div>
  `).join('');
}

// Filter-Funktion: Suchbegriff und Kategorie kombinieren
function filtereDaten() {
  const suchbegriff = document.querySelector('#suche').value.toLowerCase();
  const kategorie = document.querySelector('#filter-kategorie').value;

  let gefiltert = alleDaten;

  // Nach Suchbegriff filtern
  if (suchbegriff) {
    gefiltert = gefiltert.filter(obj =>
      obj.name.toLowerCase().includes(suchbegriff) ||
      obj.beschreibung.toLowerCase().includes(suchbegriff)
    );
  }

  // Nach Kategorie filtern
  if (kategorie) {
    gefiltert = gefiltert.filter(obj => obj.kategorie === kategorie);
  }

  zeigeErgebnisse(gefiltert);
}

// Event-Listener: Bei jeder Eingabe filtern
document.querySelector('#suche').addEventListener('input', filtereDaten);
document.querySelector('#filter-kategorie').addEventListener('change', filtereDaten);

// Daten laden und initial anzeigen
async function init() {
  const response = await fetch('objekte.json');
  alleDaten = await response.json();
  zeigeErgebnisse(alleDaten);
}

init();

Schlüsselkonzept: Daten und Anzeige trennen

Speichern Sie die Originaldaten in einer Variable (alleDaten). Filtern und sortieren Sie immer auf Basis der Originaldaten, nicht der aktuell angezeigten Daten. So können Filter jederzeit zurückgesetzt werden.

Sortierung hinzufügen

<select id="sortierung">
  <option value="name">Name (A-Z)</option>
  <option value="name-desc">Name (Z-A)</option>
  <option value="jahr">Jahr (aufsteigend)</option>
  <option value="jahr-desc">Jahr (absteigend)</option>
</select>

<script>
function sortiereDaten(daten) {
  const sortierung = document.querySelector('#sortierung').value;
  const kopie = [...daten]; // Kopie, um Original nicht zu veraendern

  switch (sortierung) {
    case 'name':
      return kopie.sort((a, b) => a.name.localeCompare(b.name));
    case 'name-desc':
      return kopie.sort((a, b) => b.name.localeCompare(a.name));
    case 'jahr':
      return kopie.sort((a, b) => a.jahr - b.jahr);
    case 'jahr-desc':
      return kopie.sort((a, b) => b.jahr - a.jahr);
    default:
      return kopie;
  }
}

// In filtereDaten() einbauen:
function filtereDaten() {
  // ... filtern wie oben ...
  const sortiert = sortiereDaten(gefiltert);
  zeigeErgebnisse(sortiert);
}
</script>

8. Fehlerbehandlung

Fehler gehören zum Programmieralltag. Besonders beim Laden externer Daten kann vieles schiefgehen: Server nicht erreichbar, ungültiges JSON, fehlende Felder. Gute Fehlerbehandlung macht den Unterschied zwischen einer robusten und einer fragilen Anwendung.

try/catch

async function ladeDaten(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`Server-Fehler: ${response.status}`);
    }

    const daten = await response.json();
    return daten;

  } catch (error) {
    // error.message enthält die Fehlerbeschreibung
    console.error('Fehler:', error.message);
    return null;
  }
}

HTTP-Statuscodes verstehen

CodeBedeutungHäufige Ursache
200OKAlles in Ordnung
301Moved PermanentlyURL hat sich geändert
400Bad RequestUngültige Parameter
401UnauthorizedFehlender/ungültiger API-Key
403ForbiddenKein Zugriff erlaubt
404Not FoundURL existiert nicht
429Too Many RequestsRate Limit überschritten
500Internal Server ErrorServerproblem

Benutzerfreundliche Fehlermeldungen

function zeigeFehler(container, fehlerTyp) {
  const meldungen = {
    netzwerk: 'Die Daten konnten nicht geladen werden. Bitte prüfen Sie Ihre Internetverbindung.',
    server: 'Der Server ist momentan nicht erreichbar. Bitte versuchen Sie es später.',
    format: 'Die geladenen Daten haben ein unerwartetes Format.',
    leer: 'Keine Ergebnisse gefunden. Versuchen Sie einen anderen Suchbegriff.'
  };

  container.innerHTML = `
    <div class="fehler-meldung">
      <p><strong>Hinweis:</strong> ${meldungen[fehlerTyp] || 'Ein unbekannter Fehler ist aufgetreten.'}</p>
    </div>
  `;
}

// Verwendung
async function ladeUndZeige() {
  const container = document.querySelector('#ergebnisse');
  container.innerHTML = '<p>Daten werden geladen...</p>';

  try {
    const response = await fetch('daten.json');
    if (!response.ok) {
      zeigeFehler(container, 'server');
      return;
    }

    const daten = await response.json();
    if (daten.length === 0) {
      zeigeFehler(container, 'leer');
      return;
    }

    zeigeErgebnisse(daten);

  } catch (error) {
    zeigeFehler(container, 'netzwerk');
  }
}

Prompt-Beispiel

„Meine JavaScript-Funktion lädt JSON-Daten mit fetch. Manchmal bekomme ich den Fehler 'Unexpected token < in JSON'. Was bedeutet das und wie kann ich es beheben?“

9. Context Engineering für Agentic Coding

Context Engineering ist die Kunst, einem LLM genau die Informationen zu geben, die es braucht, um korrekten und passenden Code zu generieren. In dieser Einheit haben wir mit APIs, JSON-Daten und komplexen Datenstrukturen gearbeitet — genau die Art von Information, die ein LLM als Kontext braucht.

Warum ist Kontext so wichtig?

Ein LLM kennt allgemeine Programmierkonzepte, aber nicht die spezifische Datenstruktur, mit der Sie arbeiten. Wenn Sie es bitten, „zeige Europeana-Ergebnisse an“, rät es die Struktur. Wenn Sie ihm die tatsächliche Response geben, schreibt es Code, der sofort funktioniert.

Die fünf Kontexttypen

  1. Datenstruktur: Ein konkretes JSON-Beispiel aus Ihrer API oder Datei
    Hier ist ein Beispiel-Objekt aus meiner JSON-Datei:
    {
      "id": "KM-001",
      "name": "Richtbeil",
      "datierung": "1798",
      "kategorie": "Strafvollzug"
    }
  2. API-Dokumentation: Endpunkte, Parameter, Response-Format
    Die Europeana API hat folgenden Endpunkt:
    GET /record/v2/search.json?query=...&rows=...
    Die Response enthält: items (Array), totalResults (Zahl)
  3. Bestehender Code: Der aktuelle Stand Ihres Projekts
    Hier ist mein bisheriger Code:
    [HTML- und JavaScript-Code einfuegen]
  4. Constraints: Technische Einschränkungen
    Verwende nur Vanilla JavaScript, kein Framework.
    Die Seite muss ohne Server funktionieren (file://).
    Keine externen Bibliotheken.
  5. Erwartetes Ergebnis: Was genau soll passieren
    Die Funktion soll die Daten als Karten-Grid (3 Spalten)
    in einem div#ergebnisse darstellen. Jede Karte zeigt:
    Name, Datierung, Kategorie und Beschreibung.

Schlüsselkonzept: Relevanter Kontext

Mehr Kontext ist nicht immer besser. Geben Sie dem LLM relevante Informationen: die Datenstruktur, die Zielstruktur und die Constraints. Irrelevante Informationen (wie die gesamte CSS-Datei, wenn es nur um JavaScript geht) können das LLM verwirren.

Effektive Prompts für API-Arbeit

Prompt-Beispiel: JSON-Daten anzeigen

„Ich habe eine JSON-Datei mit Museumsobjekten. Hier ist die Struktur:

[{"id": "KM-001", "name": "Richtbeil", "datierung": "1798", "kategorie": "Strafvollzug", "beschreibung": "..."}]

Schreibe eine async-Funktion, die diese Datei mit fetch lädt, die Objekte als Karten in einem div#sammlung darstellt und eine Live-Suche über ein input#suche ermöglicht. Verwende Vanilla JavaScript, map() für die HTML-Erzeugung und filter() für die Suche.“

Prompt-Beispiel: API-Integration

„Hier ist die Response der Europeana Search API:

{"success": true, "totalResults": 245, "items": [{"title": ["Brief..."], "edmPreview": ["https://...jpg"], "year": ["1887"], "dataProvider": ["Uni Graz"]}]}

Beachte: title, edmPreview, year und dataProvider sind Arrays, nicht Strings. Schreibe Code, der die Items als Bildergalerie darstellt. Verwende Optional Chaining (?.) für fehlende Werte.“

Prompt-Beispiel: Fehlerbehandlung

„Mein Code lädt Daten von einer API. Erweitere ihn um robuste Fehlerbehandlung: try/catch für Netzwerkfehler, Prüfung von response.ok, benutzerfreundliche Fehlermeldungen im DOM (nicht nur console.error) und eine Ladeanimation während des Ladens.“

Context Engineering in der Praxis: Schritt für Schritt

Daten verstehen

Bevor Sie prompts schreiben, schauen Sie sich die Daten an. Öffnen Sie die JSON-Datei oder die API-Response im Browser. Verstehen Sie die Struktur: Welche Felder gibt es? Welche Typen haben die Werte? Gibt es verschachtelte Objekte?

Beispiel-Objekt extrahieren

Kopieren Sie ein einzelnes, repräsentatives Objekt aus den Daten. Das ist der wichtigste Kontext für das LLM. Formatieren Sie es mit JSON.stringify(obj, null, 2) für bessere Lesbarkeit.

Ziel beschreiben

Beschreiben Sie genau, was das Ergebnis sein soll: Welche HTML-Struktur? Welche Funktionalität? Welche CSS-Klassen? Wo werden die Daten eingefügt?

Constraints angeben

Teilen Sie dem LLM mit, was Sie verwenden dürfen und was nicht: Vanilla JS, keine Frameworks, bestimmte Browser-Kompatibilität, keine externen Libraries.

Iterieren

Prüfen Sie das generierte Ergebnis. Wenn etwas nicht stimmt, geben Sie dem LLM die genaue Fehlermeldung oder beschreiben Sie, was anders sein soll. Fügen Sie bei Bedarf weiteren Kontext hinzu.

Agentic Coding: Context Engineering

Context Engineering ist die wichtigste Kompetenz beim Arbeiten mit LLMs für Code. Es geht nicht darum, den perfekten Prompt zu schreiben, sondern darum, dem LLM die richtigen Informationen zu geben: Datenstrukturen, API-Dokumentation, bestehenden Code und klare Constraints. Je besser der Kontext, desto besser der generierte Code.