28 DOM-Manipulation und Events

Das dynamische Verhalten moderner Webanwendungen basiert auf der Fähigkeit, das DOM zu manipulieren und auf Benutzerereignisse zu reagieren. Dieser Abschnitt behandelt die grundlegenden und fortgeschrittenen Techniken zur DOM-Manipulation sowie das Event-System in JavaScript.

28.1 Grundlegende DOM-Manipulation

Die Manipulation des DOM lässt sich in mehrere Kategorien unterteilen: Elemente erstellen, modifizieren, einfügen und entfernen.

28.1.1 Elemente erstellen

// Beispiel: Neue DOM-Elemente erstellen
const paragraph = document.createElement('p');
const textNode = document.createTextNode('Dieser Text wurde dynamisch erstellt.');
paragraph.appendChild(textNode);

// Alternativ direktes Setzen von innerHTML
paragraph.innerHTML = 'Dieser Text wurde <strong>dynamisch</strong> erstellt.';

28.1.2 Attribute manipulieren

// Beispiel: Setzen und Modifizieren von Attributen
const link = document.createElement('a');

// Klassische Methoden
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
const href = link.getAttribute('href');
link.removeAttribute('target');

// Direkter Zugriff auf Eigenschaften
link.id = 'main-link';
link.className = 'external-link highlighted';
link.href = 'https://updated-example.com';

// Verwaltung von Klassen mit dem classList-API
link.classList.add('new-class');
link.classList.remove('highlighted');
link.classList.toggle('active');  // Fügt Klasse hinzu, wenn nicht vorhanden, sonst entfernt
link.classList.contains('external-link'); // Prüft, ob eine Klasse vorhanden ist

28.1.3 Elemente in den DOM einfügen

// Beispiel: Einfügen von Elementen an verschiedenen Positionen
const container = document.querySelector('.container');
const existingChild = container.querySelector('.existing-child');

// Am Ende eines Elements einfügen
container.appendChild(paragraph);

// Vor einem Referenzelement einfügen
container.insertBefore(link, existingChild);

// Moderne Methoden mit flexiblerer Positionierung
container.append(paragraph, link);  // Fügt mehrere Nodes am Ende ein
container.prepend(textNode);        // Fügt am Anfang ein
existingChild.before(paragraph);    // Fügt vor dem Element ein
existingChild.after(link);          // Fügt nach dem Element ein
existingChild.replaceWith(newElement); // Ersetzt das Element

28.1.4 Elemente entfernen

// Beispiel: Entfernen von Elementen
// Klassische Methode
container.removeChild(link);

// Moderne Methode
paragraph.remove();  // Self-removal

28.2 Inhalte modifizieren

Für die Änderung von Inhalten existieren verschiedene Ansätze mit unterschiedlichen Performance- und Sicherheitsimplikationen:

// Beispiel: Modifikation von Inhalten
// innerHTML: Schnell, aber potentielles XSS-Risiko
element.innerHTML = '<span class="highlight">Neuer Inhalt</span>';

// textContent: Sicherer, behandelt Text als reinen Text
element.textContent = 'Neuer Text & <tags> werden escaped';

// innerText: Ähnlich wie textContent, aber berücksichtigt CSS-Sichtbarkeit
element.innerText = 'Dieser Text berücksichtigt Styling';

// Node-basierter Ansatz: Sicher und flexibel
element.innerHTML = '';  // Inhalte löschen
const span = document.createElement('span');
span.textContent = 'Neuer Inhalt';
span.className = 'highlight';
element.appendChild(span);

28.3 Styling mit JavaScript

DOM-Elemente können über verschiedene Ansätze gestylt werden:

// Beispiel: Styling über JavaScript
// Inline-Styles direkt setzen
element.style.color = 'blue';
element.style.backgroundColor = '#f0f0f0';
element.style.marginTop = '20px';

// Mehrere Styles auf einmal setzen
Object.assign(element.style, {
  padding: '10px',
  borderRadius: '4px',
  boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
});

// CSSText für viele Eigenschaften
element.style.cssText = 'color: red; font-size: 16px; margin: 10px;';

// Klassen-basierter Ansatz (bevorzugt)
element.classList.add('highlighted', 'active');

In der modernen Webentwicklung ist der klassenbasierte Ansatz meist zu bevorzugen, da er Trennung von Struktur und Präsentation ermöglicht und besser mit CSS-Frameworks und Responsiveness zusammenarbeitet.

28.4 Das Event-System

JavaScript-Events sind das Fundament für interaktive Webanwendungen. Sie ermöglichen die Reaktion auf Benutzerinteraktionen und andere Aktionen.

28.4.1 Grundlegende Event-Registrierung

// Beispiel: Event-Listener hinzufügen
const button = document.querySelector('button');

// Methode 1: addEventListener (bevorzugt)
button.addEventListener('click', function(event) {
  console.log('Button wurde geklickt!', event);
});

// Methode 2: On-Property (limitiert auf einen Handler pro Event)
button.onclick = function(event) {
  console.log('Button wurde geklickt!', event);
};

// Methode 3: Inline-Event-Handler (veraltet, nicht empfohlen)
// <button onclick="handleClick()">Klick mich</button>

28.4.2 Event-Objekt und Eigenschaften

Das Event-Objekt enthält wertvolle Informationen über das ausgelöste Ereignis:

// Beispiel: Arbeiten mit dem Event-Objekt
button.addEventListener('click', function(event) {
  // Allgemeine Event-Eigenschaften
  console.log('Event-Typ:', event.type);
  console.log('Event-Ziel:', event.target);
  console.log('Aktuelle Target:', event.currentTarget);
  console.log('Zeitstempel:', event.timeStamp);
  
  // Maus-spezifische Eigenschaften bei Mausevents
  console.log('Mausposition:', event.clientX, event.clientY);
  console.log('Gedrückte Taste:', event.button);
  console.log('Alt/Shift/Ctrl gedrückt:', event.altKey, event.shiftKey, event.ctrlKey);
  
  // Standardverhalten verhindern
  event.preventDefault();
  
  // Bubbling stoppen
  event.stopPropagation();
});

28.4.3 Event-Delegation

Event-Delegation ist ein leistungsstarkes Muster zur effizienten Handhabung vieler ähnlicher Events:

// Beispiel: Event-Delegation
// Statt vieler einzelner Listener für jedes Listenelement...
document.querySelector('.list').addEventListener('click', function(event) {
  // Prüfen, ob das geklickte Element oder ein Elternelement ein Listenelement ist
  const listItem = event.target.closest('li');
  
  if (listItem) {
    console.log('Listenelement geklickt:', listItem.textContent);
    
    // Mit data-* Attributen arbeiten
    const itemId = listItem.dataset.id;
    if (itemId) {
      console.log('Item ID:', itemId);
    }
  }
});

Die Event-Delegation bietet mehrere Vorteile: - Reduzierte Anzahl an Event-Listenern - Automatische Handhabung dynamisch hinzugefügter Elemente - Geringerer Speicherverbrauch - Sauberere Codestruktur

28.4.4 Event-Phasen: Capturing und Bubbling

Events durchlaufen drei Phasen: 1. Capturing-Phase: Event wandert vom document zum Zielelement 2. Target-Phase: Event erreicht das Zielelement 3. Bubbling-Phase: Event steigt vom Zielelement zurück zum document auf

// Beispiel: Event-Phasen
// Listener für die Bubbling-Phase (Standard)
parent.addEventListener('click', e => console.log('Parent Bubbling'), false);
child.addEventListener('click', e => console.log('Child Bubbling'), false);

// Listener für die Capturing-Phase
parent.addEventListener('click', e => console.log('Parent Capturing'), true);
child.addEventListener('click', e => console.log('Child Capturing'), true);

// Bei Klick auf das Child-Element ist die Reihenfolge:
// 1. Parent Capturing
// 2. Child Capturing
// 3. Child Bubbling
// 4. Parent Bubbling

28.4.5 Custom Events

Neben den nativen Events können auch benutzerdefinierte Events erstellt und ausgelöst werden:

// Beispiel: Custom Events erstellen und auslösen
// Event erstellen
const productAddedEvent = new CustomEvent('productAdded', {
  bubbles: true,
  detail: { productId: 123, name: 'Beispielprodukt', price: 29.99 }
});

// Event auslösen
document.querySelector('.shopping-cart').dispatchEvent(productAddedEvent);

// Event abfangen
document.addEventListener('productAdded', function(event) {
  console.log('Produkt hinzugefügt:', event.detail);
  updateCartUI(event.detail);
});

Custom Events eignen sich besonders für die modulare Entwicklung und Komponenten-Kommunikation in größeren Anwendungen.

28.5 Häufige Event-Typen

Webapplikationen nutzen eine Vielzahl von Event-Typen:

28.5.1 UI-Events

// Beispiel: UI-Events
window.addEventListener('load', () => console.log('Seite vollständig geladen'));
window.addEventListener('DOMContentLoaded', () => console.log('DOM geladen, vor Bildern/Styles'));
window.addEventListener('resize', () => console.log('Fenstergröße geändert'));
window.addEventListener('scroll', () => console.log('Scrollposition geändert'));

28.5.2 Maus- und Touch-Events

// Beispiel: Maus- und Touch-Events
element.addEventListener('click', handleClick);
element.addEventListener('dblclick', handleDoubleClick);
element.addEventListener('mousedown', handleMouseDown);
element.addEventListener('mouseup', handleMouseUp);
element.addEventListener('mousemove', handleMouseMove);
element.addEventListener('mouseover', handleMouseOver);
element.addEventListener('mouseout', handleMouseOut);

// Touch-Events
element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchmove', handleTouchMove);
element.addEventListener('touchend', handleTouchEnd);

28.5.3 Keyboard-Events

// Beispiel: Tastatur-Events
document.addEventListener('keydown', function(event) {
  console.log('Taste gedrückt:', event.key, 'Keycode:', event.keyCode);
  
  // Tasten-Kombinationen erkennen
  if (event.ctrlKey && event.key === 's') {
    event.preventDefault(); // Browser-Speicherdialog verhindern
    saveDocument();
  }
});

document.addEventListener('keyup', handleKeyUp);
document.addEventListener('keypress', handleKeyPress); // Veraltet, besser keydown verwenden

28.5.4 Formular-Events

// Beispiel: Formular-Events
const form = document.querySelector('form');
form.addEventListener('submit', function(event) {
  event.preventDefault(); // Verhindert Seiten-Neuladen
  validateAndSubmit();
});

const input = document.querySelector('input[type="text"]');
input.addEventListener('input', handleInput);    // Bei jeder Änderung
input.addEventListener('change', handleChange);  // Bei Änderung und Verlassen des Feldes
input.addEventListener('focus', handleFocus);    // Bei Fokussierung
input.addEventListener('blur', handleBlur);      // Bei Fokusverlust

28.6 Fortgeschrittene Techniken

28.6.1 IntersectionObserver für Scrolleffekte

// Beispiel: IntersectionObserver für lazy loading und Scrolleffekte
const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const element = entry.target;
      
      // Lazy Loading für Bilder
      if (element.dataset.src) {
        element.src = element.dataset.src;
        element.removeAttribute('data-src');
        observer.unobserve(element);
      }
      
      // Scroll-Animation aktivieren
      element.classList.add('visible');
    }
  });
}, {
  root: null, // Viewport als Referenz
  rootMargin: '0px',
  threshold: 0.1 // 10% des Elements muss sichtbar sein
});

// Elemente beobachten
document.querySelectorAll('.lazy-image, .animate-on-scroll').forEach(el => {
  observer.observe(el);
});

28.6.2 MutationObserver für DOM-Änderungen

// Beispiel: MutationObserver für DOM-Änderungen überwachen
const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('Knoten hinzugefügt oder entfernt');
      console.log('Hinzugefügte Knoten:', mutation.addedNodes);
      console.log('Entfernte Knoten:', mutation.removedNodes);
    } else if (mutation.type === 'attributes') {
      console.log(
        `Attribut ${mutation.attributeName} von ${mutation.target.nodeName} geändert`
      );
    }
  });
});

// Konfiguration der zu beobachtenden Änderungen
observer.observe(targetNode, {
  attributes: true,
  childList: true,
  subtree: true,
  attributeOldValue: true
});

28.6.3 ResizeObserver für Elementgrößenänderungen

// Beispiel: ResizeObserver für Element-Größenänderungen
const resizeObserver = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const { width, height } = entry.contentRect;
    console.log(`Element Größe: ${width}px × ${height}px`);
    
    // Responsives Verhalten ohne Media Queries
    if (width < 600) {
      entry.target.classList.add('compact');
    } else {
      entry.target.classList.remove('compact');
    }
  });
});

resizeObserver.observe(document.querySelector('.resizable-component'));

28.7 Performanceoptimierung bei Events

Event-Handler können erhebliche Performance-Implikationen haben, besonders bei häufig auftretenden Events:

// Beispiel: Debouncing für Performance-Optimierung
function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

// Anwendungsbeispiel: Effizienter Resize-Handler
window.addEventListener('resize', debounce(function() {
  updateLayout();
}, 200));

// Anwendungsbeispiel: Effizienter Scroll-Handler
window.addEventListener('scroll', debounce(function() {
  checkScrollPosition();
}, 100));

Die folgenden Techniken verbessern die Event-Performance:

  1. Debouncing: Verzögert die Ausführung, bis eine Pause im Event-Strom auftritt
  2. Throttling: Begrenzt die Ausführungshäufigkeit auf ein bestimmtes Intervall
  3. requestAnimationFrame: Synchronisiert Event-Handler mit dem Rendering-Zyklus
  4. Passive Event Listener: Vermeidet Scrolling-Blockaden durch { passive: true }
// Beispiel: Passive Event Listener
document.addEventListener('touchstart', handleTouchStart, { passive: true });