JavaScript auf dem Client

Vorteil: Entwicklung von Desktop-artigen Applikationen

Endlich: Veränderungen auf der Seite ohne komplettes Neuladen
  • Theoretisch lassen sich die HTML-Dokumente auch ohne Server erzeugen
  • Frameworks wie Electron als Alternative zu “klassischen” UI-Frameworks

Wahlmöglichkeit: Sehr viel flexiblere Styling-Regeln als mit CSS

Durch JavaScript könnte man theoretisch sogar ohne CSS auskommen
Aus Gründen der Übersichtlichkeit und der Performance typischerweise keine gute Idee

Wahlmöglichkeit: Komplexe Benutzerinteraktionen ohne Serveranfragen

Mit JavaScript lassen sich beliebig komplizierte Applikationen entwickeln …
… am Ende des Tages müssen die Daten aber meistens trotzdem zum Server.

Wahlmöglichkeit: Wo soll der HTML-Code generiert werden?

Im Rahmen dieser Vorlesung zu clientseitigem JavaScript: Ausschließlich auf dem Server
Ergänzung von HTML-Strukturen natürlich dennoch auf dem Client technisch gesehen möglich
Grundregel wie bisher: Generierung von HTML-Code sollte auf Basis einer Templating-Mechanik erfolgen
Möglichst klare Trennung zwischen Programmcode, Daten und HTML-Erzeugung

Gefahr: Unentschlossenheit oder mangelnde Konsistenz

Komplexe JavaScript-Anwendungen brauchen ein solides Konzept
Beliebigkeit führt zu schwer wartbarem Code
Im Laufe der weiteren Veranstaltung: Clientseitige JavaScript-Frameworks
Ermöglichen eben diese Trennung auch im Browser

Einbindung von JavaScript in Webseiten

Grundsätzlich: Notation “inline” oder Laden aus Dateien
Eingebettete Schreibweise wird heutzutage als Sicherheitsrisiko eingestuft
Elemente im DOM-Baum können über allgemeine oder spezielle Ereignisse berichten
Konkrete Ereignisse können in der Dokumentation der DOM-Klassen nachgeschlagen werden

Finden von Knoten im DOM-Baum

Grundsätzlich: Für jedes HTML-Element steht eine korrespondierendes JavaScript-Objekt bereit
Manipulation aller Eigenschaften möglich
querySelector & querySelectorAll: Erlauben die Navigation des DOM mit CSS-Selektoren
Ergebnis ist exakt ein Element bzw. eine Liste von Elementen
querySelectorAll liefert kein klassisches Array zurück
Wrappen in Array.from() notwendig um Iterationsfunktionen (außer forEach) zu nutzen
Entwicklerkonsole aller gängigen Webbrowser mit sinvoller Visualisierung für DOM-Elemente
Einfacher Aufruf von document.querySelector ausreichend

Zum Testen: Ausführung über die Entwicklerwerkzeuge

In den meisten Browsern: Öffnen der Entwicklerwerkzeuge bietet eine JavaScript-Konsole
  • Eingegebene Anweisungen werden sofort ausgeführt
  • Code-Vervollständigung auf Basis der aktuell geladenen JavaScript-Programme

Konsolen-Sitzung im Firefox

Veraltet: Direkte Reaktion auf Ereignisse

Notation des JavaScript-Codes in einem on-Attribut des Elements
Keine Zeilenumbrüche, gedacht für Funktionsaufrufe
Typische Ereignisse
  • keydown & keyup: Eine Taste wurde gedrückt bzw. losgelassen
  • keypress: Eine Taste wurde gedrückt und losgelassen
  • change: Der Wert eines Eingabeelements hat sich geändert
Problematisch: Durchbricht Trennung von Zuständigkeiten
HTML-Quelltext soll Strukturen widerspiegeln, nicht Quelltext beinhalten
Im Beispiel: Zuweisung des Wertes this.value bei jedem Tastendruck
  • this steht im Kontext dieses Ereignisses für das <input>-Element
  • this.value ist der gleiche Wert, wie er mit <input value="xyz"> angegeben werden würde

Unpraktisch: Inline-Notation mit <script>

Notation des JavaScript-Codes in einem <script>-Block
Blockierende Ausführung sobald der HTML-Parser die entsprechende Stelle erreicht
Alle Blöcke teilen sich auf der obersten Ebene den gleichen Ausführungskontext
Es gibt keine <script>-lokalen Variablen
  • javascript_browser/embed-script-inline.html | Validieren
    <script type="application/javascript">
     let h1 = document.querySelector('h1');
     if (h1) {
         h1.textContent = '#1';
     } else {
         console.log('#1: Keine Überschrift gefunden');
     }
    </script>
    
    <h1>Static Content</h1>
    
    <script type="application/javascript">
     // Deklaration von oben noch gültig!
     h1 = document.querySelector('h1');
     if (h1) {
         h1.textContent = '#2';
     } else {
         console.log('#2: Keine Überschrift gefunden');
     }
    </script>
    

Typisch: Laden von externen Dateien mit <script>

Verweis auf standardmäßig eine Datei mit JavaScript-Code in einem <script>-Block mit src-Attribut
Standardmäßig: Blockierende Ausführung sobald der HTML-Parser die entsprechende Stelle erreicht

Ablauf der Ladevorgänge

Attribut defer: Verschiebt Ausführung bis das Dokument vollständig geladen ist
Ladevorgang und Kompilierung erfolgen parallel zum Parsing-Vorgang
Attribut async: Erlaubt Ladevorgang während des Parsens
Ausführung erfolgt blockierend sobald der Ladevorgang abgeschlossen ist
Attribute type mit dem Wert module: Lädt referenzierte Dokumente nach
Zur Zeit mangelhafte Unterstützung durch Browser
Zeitstrahl mit den verschiedenen Phasen des Ladevorgang

Clients ohne aktiviertes JavaScript

Historisch: Nicht jeder Browser verfügte über eine voll funktionsfähige JavaScript-Implementierung
Heute: Jeder mit Sicherheitsupdates versorgte Browser hat eine brauchbare JavaScript-Implementierung
Primäre Zielgruppe: Suchmaschinen, maschinelle Verarbeitung und sicherheitsbewusste Nutzer
Suchmaschinen und maschinelle Verarbeitung allerdings wesentlich relevanter, Schätzungsweise deaktivieren maximal 2% der Nutzer JavaScript
Sinnvolles Ziel: Grundlegende Benutzung der Seite auch ohne JavaScript ermöglichen
Zumindestens alle für Suchmaschinen relevanten Informationen im HTML-Quelltext erwähnen

Hinweis auf deaktiviertes JavaScript mit <noscript>

Inhalt von <noscript> wird bei deaktiviertem JavaScript dargestellt
Erlaubt einen freundlichen Hinweis, gegebenenfalls unter Verweis auf die Datenschutzerklärung oder ähnliche Dokumente
  • javascript_browser/noscript.html | Validieren
    <noscript>
      Diese Seite benötigt JavaScript um wichtige Informationen anzuzeigen.
    </noscript>
    
  • javascript_browser/noscript.js
    const headerElem = document.createElement('h1');
    headerElem.textContent = 'Wichtige Information';
    
    const textElem = document.createElement('p');
    textElem.textContent = 'The narwhal bacons at midnight.';
    
    [headerElem, textElem].forEach(elem => {
      document.body.appendChild(elem);
    });
    

Vom HTML-Quelltext zum DOM-Baum

Beim Parsen des HTML-Quelltextes wird ein DOM-Baum generiert
Repräsentation des HTML-Dokuments im Speicher
Objektorientierte Programmierschnittstelle um zur Laufzeit Manipulationen vorzunehmen
Transformationen der textuellen HTML-Darstellung umständlich und fehleranfällig

Grundlegende Klassen

G domEventTarget EventTarget domNode Node domNode->domEventTarget domElement Element domElement->domNode domDocument Document domDocument->domNode domHtmlElement HTMLElement domHtmlElement->domElement domSvgElement SVGElement domSvgElement->domElement
EventTarget: Basisklasse für alles, was ein Ereignis auslösen könnte
Definiert die grundlegenden Methoden um Callbacks zu registrieren
Node: Basisklasse für alle Knoten im DOM-Baum
Erlaubt Zugriffe auf die Struktur des Baumes
Document: Wurzel des DOM-Baums
Kennt die grundlegenden Parameter des Parsing-Vorgangs, zum Beispiel das Encoding
Element: Basisklasse für alle visuellen Knoten die Teil eines Dokumentes sind
Kennen Dimensionen und weitere Eigenschaften
HTMLElement & SVGElement: Konkrete Klassen für HTML- oder SVG-Knoten
Enthalten spezifische Informationen für die jeweilige Auszeichnungssprache

HTML-Attribute und Eigenschaften von DOM-Objekten

Grundsätzlich: Alle CSS- und HTML-Eigenschaften lassen sich via JavaScript manipulieren
Verwendung aber nur mit Bedacht!
Faustregel: CSS-Eigenschaften befinden sich im Unterobjekt style
Unmittelbare Zuweisung an andere Eigenschaften möglich, aber ohne Effekt
Faustregel: HTML-Eigenschaften werden direkt zugewiesen
Für einige Eigenschaften (insbesondere class) aber noch besondere Hilfsmethoden
Werte einiger HTML-Eigenschaften überschneiden sich mit CSS, z.B. width und height
Vor allem bei <canvas> und <img>-Elementen problematisch
  • javascript_browser/dom-attributes.html | Validieren
    <div>
      <label>
        Eingabe 1 <input type="text" id="fst">
      </label>
    </div>
    <div>
      <label>
        Eingabe 2 <input type="text" id="snd">
      </label>
    </div>
    
    
  • javascript_browser/dom-attributes.js
    const eleFst = document.querySelector("#fst");
    const eleSnd = document.querySelector("#snd");
    
    // Korrekte und sinnvolle Zuweisungen
    eleFst.value = "first";
    eleFst.style.backgroundColor = "blue";
    eleFst.style.width = "50px";
    
    // Inkorrekte Zuweisungen
    eleSnd.style.value = "first";
    eleSnd.backgroundColor = "red";
    eleSnd.width = 200;
    

Eigene Annotation von Elementen mit data-Attributen

Theoretische Möglichkeit: Ergänzung von eigenen Attributen und Eigenschaften zu Elementen
Praktische Folge: Unübersichtliches Chaos
Daher: Einführung des data-Namensraums für Ergänzungen von Benutzern
  • Notation im HTML-Quelltext möglich, Zugriff im DOM über dataset-Eigenschaft
  • Werden vom Browser in der Verarbeitung ignoriert
Nutzung sollte nur sporadisch erfolgen, oft bessere Alternativen verfügbar
  • Für grafische Darstellungen sollten unbedingt CSS-Klassen genutzt werden
  • Für viele Daten-Annotierungen kann man auch gleich zu semantischer Auszeichnung greifen
  • javascript_browser/element-data-attributes.html | Validieren
    <ul>
      <li data-price="5">Kostet 5 Geld</li>
      <li data-price="12">12 Geld, Superangebot!</li>
      <li data-price="15">2 zum Preis von 3 für 15</li>
    </ul>
    <p>Gesamtpreis: <span id="totalPrice"></span></p>
    
  • javascript_browser/element-data-attributes.js
    const elements = document.querySelectorAll("[data-price]");
    const price = Array.from(elements)
          .map(e => +e.dataset["price"])
          .reduce((lhs, rhs) => lhs + rhs);
    
    const targetElement = document.querySelector("#totalPrice");
    targetElement.textContent = price;
    

Debugging im Browser

Klassische Möglichkeit: Debugausgaben mit console.log
  • Ausgabe erfolgt auf der Konsole in den Entwicklertools
  • Komplexe Datenstrukturen lassen sich interaktiv untersuchen
Klassische, aber obsolete Möglichkeit: Debugausgaben mit alert
  • Ausgabe erfolgt in einem Dialog
  • Visualierung ist immer ein String

Eingebaute Debugging-Werkzeuge

In den meisten Entwicklerwerkzeugen: Debugger für clientseitigen Code verfügbar
  • Inspizieren von lokalen und globalen Variablen
  • Code schrittweise ausführen
Spezielle Anweisung debugger zum Setzen eines Haltepunktes
Hat keinen Effekt, wenn der Debugger nicht aktiviert ist

Debugger im Firefox

Debugging Beispiel

  • javascript_browser/debugger-intro.html | Validieren
    <h1>Debugging Beispiel</h1>
    <p>
      Bei aktivierter Entwicklerkonsole sollte bei
      diesem Dokument der Debugger aktiviert werden.
      <ul>
        <li>Schere</li>
        <li>Stein</li>
        <li>Papier</li>
      </ul>
    </p>
    
  • javascript_browser/debugger-intro.js
    const heading = document.querySelector('h1');
    heading.textContent = heading.textContent.toUpperCase();
    debugger;
    
    document.querySelectorAll('li').forEach((elem, idx) => {
      debugger;
      elem.textContent = idx + " " + elem.textContent;
    });

Reaktion auf Ereignisse und Manipulationen im DOM-Baum

Registrieren von Ereignissen mit der addEventListener
Methode der Klasse EventTarget und damit auf faktisch allen Objekten verfügbar
  1. Parameter: Name des Ereignisses (change, click, …)
  2. Parameter: Callback-Funktion
Signatur der Callback-Funktion
  1. Parameter: Ausgelöstes Ereignis, wird typischerweise event genannt
Aufruf der Callback-Funktion erfolgt im Kontext des auslösenden Elements
Zugreifbar über Schlüsselwort this
Spezifische Ereignisse für spezielle Elemente
Nicht alle Objekte können alle Ereignisse auslösen

Ausführung von Skripten bei fertig geladenem DOM-Baum

Optimalerweise: Mit dem defer-Attribut Ausführungszeitpunkt auf nach dem Parsen festlegen
Wird für die meisten Anwendungsfälle hinreichend gut unterstützt
Sicherheitshalber: Nutzung des DOMContentLoaded-Ereignisses
Wird gefeuert sobald das DOM geladen ist, ein bisschen besser unterstützt
Gruselig: Internet Explorer vor Version 9

In earlier versions of Internet Explorer, this state can be detected by repeatedly trying to execute document.documentElement.doScroll("left");, as this snippet will throw an error until the DOM is ready.

Das click-Ereignis

Typ des Ereignis-Objektes ist MouseEvent
Wird für viele weitere Ereignisse genutzt

Weitere Mausereignisse

mousein, mouseout und mouseover sind ebenfalls vom Typ MouseEvent
Vorher CSS-Alternativen prüfen, dieses Beispiel ist kein “best practice”!
  • javascript_browser/event-mouse.html | Validieren
    <ul>
      {% for i in (1..5) %}
        <li style="color: blue;">{{ i }}</li>
      {% endfor %}
    </ul>
    
  • javascript_browser/event-mouse.js
    const allListItems = Array.from(document.querySelectorAll('li'));
    
    allListItems.forEach(item => {
      item.addEventListener('mouseover', function(event) {
        this.textContent = +this.textContent + 1 ;
      });
    
      item.addEventListener('mouseenter', function(event) {
        this.style = "color: red;";
      });
    
      item.addEventListener('mouseout', function(event) {
        this.style = "";
      });
    });
    

Mausereignisse mit data-Attribut

Ablegen von Zähler- und Darstellungsinformationen in data-Attributen
Bei vorsichtiger Absprache: Können von Designern vorgegeben und von Entwicklern verarbeitet werden
Grenzwertig komplizierte Logik, nur in Ausnahmefällen zu nutzen
Auf keinen Fall ein “eigenes” Web-Framework auf Grundlage von data-Attributen schaffen
  • javascript_browser/event-mouse-data.html | Validieren
    <ul>
      {% for i in (1..5) %}
        <li data-count="0"
            data-text="Anzahl Sichtungen: ">
          {{ i }} hat noch nie einen Mauszeiger gesehen
        </li>
      {% endfor %}
    </ul>
    
  • javascript_browser/event-mouse-data.js
    const allListItems = Array.from(document.querySelectorAll('li'));
    
    allListItems.forEach(item => {
      item.addEventListener('mouseover', function(event) {
        this.dataset.count = +this.dataset.count + 1;
        this.textContent = this.dataset.text + this.dataset.count;
      });
    });
    

Hinzufügen oder entfernen von Klassen mit classList

add(String[, String]) fügt eine variable Anzahl von Klassen hinzu
remove(String[, String]) entfernt eine variable Anzahl von Klassen
Parameterliste hat eine variable Anzahl von Argumenten, Übergabe einer Liste ist ein Fehler
toggle(String) alterniert die Existenz einer Klasse als Teil der Klassenliste
Sofern die angegebene Klasse Teil der Liste ist wird sie entfernt, andernfalls hinzugefügt
toggle(String, Boolean) fügt die gegebene Klasse hinzu oder entfernt Sie
true als zweiter Parameter fügt hinzu, false entfernt
contains(String) prüft, ob die angegebene Klasse gesetzt ist
Funktioniert nur für exakt eine Klasse

Beispiel: “ToDo”-Liste

  • javascript_browser/event-mouse-classlist.html | Validieren
    <h1>Fruitcake</h1>
    <ul>
      <li class="done">Apple</li>
      <li>Mighty Banana</li>
      <li class="done">Wildberry</li>
      <li>Tabantha Wheat</li>
      <li>Cane Sugar</li>
    </ul>
    
  • javascript_browser/event-mouse-classlist.js
    const elements = Array.from(document.querySelectorAll('ul li'));
    const heading = document.querySelector('h1');
    
    elements.forEach(element => {
      element.addEventListener('click', function(event) {
        this.classList.toggle('done');
    
        const allTicked = elements.every(v => v.classList.contains('done'));
        heading.classList.toggle('done', allTicked);
      });
    });
    
  • javascript_browser/event-mouse-classlist.css
    ul li.done {
        text-decoration: line-through;
    }
    
    h1.done {
        color: green;
    }
    

Reaktionen auf Eingabeveränderungen

Zugriff auf Eingabelemente aus JavaScript heraus möglich
value-Eigenschaft lässt sich einfach auslesen
Reaktionen auf Veränderungen mit Ereignissen
  • javascript_browser/event-input-change.html | Validieren
    <input type="text" placeholder="Name?">
    <ul>
      <li><code>change</code>: <span id="change"></span></li>
      <li><code>keyup</code>: <span id="keyup"></span></li>
    </ul>
    
  • javascript_browser/event-input-change.js
    const changeElem = document.querySelector('#change');
    const keyupElem = document.querySelector('#keyup');
    const inputElem = document.querySelector('input');
    
    inputElem.addEventListener('change', event => {
      changeElem.textContent = inputElem.value;
    });
    
    inputElem.addEventListener('keyup', event => {
      keyup.textContent = inputElem.value;
    });
    

Sub-Selektionen

Ziel: Arbeiten auf Elementen in einem DOM-Teilbaum
Zum Beispiel bei wiederholten Elementen
Vorgehen: querySelector auf dem Wurzel-Element des Teilbaums aufrufen
Ergebnis ist dann auf Kinder des Wurzel-Elements beschränkt
  • javascript_browser/sub-selection.html | Validieren
    {% for i in (1..5) %}
      <div class="input-mirror">
        <input type="text" placeholder="Name?">
        <ul>
          <li><code>change</code>: <span class="change"></span></li>
          <li><code>keyup</code>: <span class="keyup"></span></li>
        </ul>
      </div>
    {% endfor %}
    
  • javascript_browser/sub-selection.js
    const parentElems = document.querySelectorAll('.input-mirror');
    
    parentElems.forEach(elem => {
      const changeElem = elem.querySelector('.change');
      const keyupElem = elem.querySelector('.keyup');
      const inputElem = elem.querySelector('input');
    
      inputElem.addEventListener('change', event => {
        changeElem.textContent = inputElem.value;
      });
    
      inputElem.addEventListener('keyup', event => {
        keyupElem.textContent = inputElem.value;
      });
    });
    

Filtern von angezeigten Elementen

Ziel: Nicht alle, sondern nur einige Datensätze anzeigen
  • Auf Basis von Namensbestandteilen, Tags, Preisen …
  • Zur Erinnerung: Tatsächliches Löschen von Elementen (im Rahmen der Übung) strikt verboten
Vorgehen: Definition einer Hilfsfunktion filterElements
  • Entfernt zunächst die Klasse gone von allen Elementen …
  • … und fügt Sie dann auf Basis einer Callback-Funktion selektiv wieder hinzu

Beispiel: Filtern von angezeigten Elementen (I)

Probleme dieser initialen Version
  • Kopfzeile der Tabelle wird ebenfalls gefiltert
  • Keine Berücksichtigung von Groß- und Kleinschreibung
  • javascript_browser/filter-table.html | Validieren
    <label>Filter: <input id="search">
    <table>
      <tr>
        <th>Name</th>
        <th>Zutaten</th>
      </tr>
      {% for product in page.products %}
        <tr>
          <td>{{ product.name }}</td>
          <td>{{ product.ingredients }}</td>
        </tr>
      {% endfor %}
    </table>
    
  • javascript_browser/filter-table.js
    const filterElements = (elem, selector, criteriaCallback) => {
      const elements = Array.from(elem.querySelectorAll(selector));
      // Grundzustand: Klasse "gone" von allen Elementen entfernen
      elements.forEach(e => {
        e.classList.remove("gone")
      });
    
      // Filterzustand: Klasse "gone" selektiv zu hinzufügen
      elements
        .filter(criteriaCallback)
        .forEach(e => {
          e.classList.add("gone")
        });
    };
    
    const searchElem = document.querySelector('#search');
    const tableElem = document.querySelector('table');
    
    searchElem.addEventListener('keyup', event => {
      const searchText = searchElem.value;
    
      filterElements(tableElem, "tr", (rowElem) => {
        return (rowElem.textContent.indexOf(searchText) === -1);
      });
    });

Beispiel: Filtern von angezeigten Elementen (II)

Korrigiert: Tabellenüberschriften werden nicht mehr gefiltert
Ergänzung des Query-Selektors um :not(:first-child)
Korrigiert: Groß- und Kleinschreibung ignorieren
Umwandlung des Suchstrings und des Textes in Kleinbuchstaben
  • javascript_browser/filter-table-v2.html | Validieren
    <label>Filter: <input id="search">
    <table>
      <tr>
        <th>Name</th>
        <th>Zutaten</th>
      </tr>
      {% for product in page.products %}
        <tr>
          <td>{{ product.name }}</td>
          <td>{{ product.ingredients }}</td>
        </tr>
      {% endfor %}
    </table>
    
  • javascript_browser/filter-table-v2.js
    const filterElements = (elem, selector, criteriaCallback) => {
      const elements = Array.from(elem.querySelectorAll(selector));
      elements.forEach(e => {
        e.classList.remove("gone")
      });
    
      elements
        .filter(criteriaCallback)
        .forEach(e => {
          e.classList.add("gone")
        });
    };
    
    const searchElem = document.querySelector('#search');
    const tableElem = document.querySelector('table');
    
    searchElem.addEventListener('keyup', event => {
      const searchText = searchElem.value;
    
      filterElements(tableElem, "tr:not(:first-child)", (rowElem) => {
        const lowerSearch = searchText.toLowerCase();
        const lowerText = rowElem.textContent.toLowerCase();
        return (lowerText.indexOf(lowerSearch) === -1);
      });
    });

Unterschied zwischen arrow und function Schreibweise: this-Kontext

In JavaScript haben alle Funktionsaufrufe einen impliziten this-Kontext
Im Zweifel der globale Kontext
arrow-Notation von Funktionen übernimmt this-Kontext des Elternelements
Und erhält damit bei Ereignisbehandlungen nicht den gewünschten Kontext
Zugriff auf das auslösende Element über target-Eigenschaft des Ereignisses
Funktioniert für beide Schreibweisen

Asynchronous JavaScript and XML (AJAX)

Ziel: Kommunikation mit dem Server ohne kompletten Neuaufbau der Seite
  • Für Webseiten: Austausch von HTML-Fragmenten auf der Seite
  • Für Daten: Aufruf einer rein serverseitigen Funktion
Mittel: XMLHttpRequest-Klasse
  • XML im Namen ist irreführend, es können beliebige Daten übertragen werden
  • Http im Namen ist irreführend, es können auch andere Protokolle verwendet werden
Ereignisse vom Typ ProgressEvent berichten über den Fortschritt einer Anfrage
  • Ereignis progress wird wiederholt ausgelöst, wenn sich der Fortschritt signifikant geändert hat
  • Ereignis load wird ausgelöst, wenn der Ladevorgang erfolgreich beendet ist
  • Ereignis error wird bei fehlgeschlagener Kommunikation ausgelöst, ein StatusCode != 200 ist kein Fehler, sondern eine Antwort
  • Ereignis abort wird ausgelöst, wenn der Benutzer die Anfrage unterbrochen hat
  • Ereignis loadend wird ausgelöst, wenn der Ladevorgang aus irgendeinem Grund beendet worden ist
Aufruf der Methoden open() und send() um die Anfrage tatsächlich zu stellen
Ereignisverarbeitung sollte unbedingt vorher schon definiert worden sein
open(httpVerb, url)
Same Origin Policy: Ohne weitere Maßnahmen muß die Ziel-URL auf den exakt gleichen Host wie die ladende Seite zeigen
send([data])
Tatsächliches Abschicken der Anfrage, für POST-Anfragen zusätzlich: Angabe von Daten, die im Rumpf verschickt werden sollen

Ablauf einer AJAX-Anfrage

Dieses Beispiel demonstriert den Lebenszyklus einer AJAX-Abfrage
Und zeigt den Zugriff auf XMLHttpRequest.statusCode, um ausgeführte aber nicht-erfolgreiche Anfragen zu erkennen
  • javascript_browser/ajax-simple.html | Validieren
    <input type="text" placeholder="URL?">
    <button>AJAX-Anfrage abschicken</button>
    <pre></pre>
    
  • javascript_browser/ajax-simple.js
    const targetUrlElem = document.querySelector('input');
    const sendBtn = document.querySelector('button');
    const resultElem = document.querySelector('pre');
    
    const logCallback = function(name, event) {
      console.log(`${name}:`, event)
    
      let resultText = "";
      if (event.type === 'error') {
        resultText = "Entwicklertools geben Aufschluss über Art des Fehlers";
      } else {
        resultText = `${event.target.status} - ${event.target.responseURL}`;
      }
      resultElem.textContent += `${name}: ${resultText}\n`
    }
    
    sendBtn.addEventListener('click', event => {
      sendBtn.disabled = true;
      
      const req = new XMLHttpRequest();
    
      req.addEventListener("loadend", event => {
        sendBtn.disabled = false;    
      });
    
      req.addEventListener("progress", e => logCallback("Fortschritt", e));
      req.addEventListener("load", e => logCallback("Geladen", e));
      req.addEventListener("error", e => logCallback("Fehler", e));
      req.addEventListener("abort", e => logCallback("Abbruch", e));
    
      req.open("GET", targetUrlElem.value);
      req.send();
    });
    

Ersetzen von DOM-Teilbäumen

Auf dem Server gerenderte HTML-Fragmente in den eigenen Quelltext einsetzen
  • XMLHttpRequest-Objekte verfügen nach erfolgreicher Durchführung über eine responseText-Eigenschaft mit der Antwort des Servers
  • DOM-Elemente verfügen über eine innerHTML-Eigenschaft, mit der zur Laufzeit ein neuer Teilbaum aus einem String erzeugt werden kann
event.preventDefault() verhindert “normale” Verarbeitung durch den Browser
Seite wird nicht neu geladen
  • javascript_browser/ajax-replace.html | Validieren
    <label>
      Thema der Anfrage:
      <select>
        {% for topic in page.available-topics %}
          <option>{{ topic }}</option>
        {% endfor %}
      </select>
    </label>
    <button>(Erneut?) Abschicken</button>
    <div id="result"></div>
    
  • javascript_browser/ajax-replace.js
    const selectElem = document.querySelector('select');
    const btnSendElem = document.querySelector('button');
    const resultHostElem = document.querySelector('#result');
    
    const sendAndEmbedRequest = function() {
      selectElem.disabled = true;
      btnSendElem.disabled = true;
      
      const req = new XMLHttpRequest();
      req.addEventListener('loadend', _ => {
        selectElem.disabled = false;
        btnSendElem.disabled = false;
        if (req.status === 200) {
          resultHostElem.innerHTML = req.responseText;
        }
      });
      
      req.open("GET", `/interactive/api/random/${selectElem.value}`);
      req.send();
    }
    
    selectElem.addEventListener('change', sendAndEmbedRequest);
    btnSendElem.addEventListener('click', sendAndEmbedRequest);
    
    sendAndEmbedRequest();
    

Serverseitiger Code für AJAX-Anfragen

Zugriff erfolgt über ganz normale Routen
Gleich zu sehen: POST-Anfragen lassen sich vom Client einfacher parametrisieren
Ergebnis der Route sollte kein vollständiges HTML-Dokument sein
Sondern stattdessen ein Fragment, dass exakt an die entsprechende Stelle im DOM passt
Weiterhin: Verwendung der gleichen Templating-Vorgehensweisen wie für “normale” Seiten
Für die Übung also .hbs-Dateien für den HTML-Code des Fragments und eine Route, welche die entsprechenden Daten berechnet
Einzige Änderung: Neuer Rückgabetyp fragment für Routen
Funktioniert ähnlich wie page, aber:
  • Sucht den angegebenen Dateinamen im templates-Ordner
  • Führt keine Validierung durch

Umgang mit Formularen

Typischer Fehler: “Normale” HTML-Formular-Semantiken aufgrund von verfügbarem JavaScript vollständig ignorieren
Wie im Beispiel zur Ersetzung von DOM-Teilbäumen: <form>-Element fehlt völlig
Ergebnis: Quelltext wird vor lauter Selektoren für verschiedene Elemente schnell unübersichtlich
Außerdem ist die automatische Validierung ohne übergeordnetes Formular nicht sinnvoll möglich
Daher: Im JavaScript nicht direkt mit den value-Eigenschaften arbeiten
Sondern stattdessen lieber mit FormData-Objekten
FormData erlaubt den Zugriff auf alle Daten eines Formulares in exakt der Form, wie sie an den Server gesendet werden würden
  • Neue Instanz von FormData erzeugen mit const formData = new FormData(formElem);
  • Dabei steht formElem für ein mit z.B. document.querySelector() erhaltenes Formularelement
  • Zugriff auf einzelne Werte dann mit formData.get(keyString) möglich
Positiver Seiteneffekt dieses Ansatzes: Die Seite ist für Besucher ohne aktiviertes JavaScript benutzbar
Hilft nicht nur Menschen mit Sicherheitsbewusstsein oder Behinderungen, sondern auch (Such-)Maschinen
Allerdings: Bei serverseiten Routen mit Parametern (z.B. /person/:id/name) überschaubarer Mehraufwand nötig
  • Formulare erzeugen aktuell grundsätzlich GET-Anfragen mit parametriesierten URLs oder POST-Anfragen
  • Parameter als Teil der Pfadangaben können ohne JavaScript nicht ausgedrückt werden

Korrekt ausgezeichnetes AJAX-Formular mit FormData

FormData-Instanz kann einem <form>-DOM-Element erzeugen
Kann über XMLHttpRequest.send() bequem an den Server übertragen werden
  • javascript_browser/ajax-form-data.html | Validieren
    <form method="POST" action="/interactive/api/random">
      <label>
        Thema der Anfrage:
        <select name="topic">
          {% for topic in page.available-topics %}
            <option>{{ topic }}</option>
          {% endfor %}
        </select>
      </label>
      <input type="submit" value="(Erneut?) Abschicken">
    </form>
    <div id="result"></div>
    
  • javascript_browser/ajax-form-data.js
    const formElem = document.querySelector('form');
    const resultHostElem = document.querySelector('#result');
    
    const sendAndEmbedRequest = event => {
      if (event) {
        event.preventDefault();
      }
      
      const req = new XMLHttpRequest();
      req.addEventListener('loadend', _ => {
        if (req.status === 200) {
          resultHostElem.innerHTML = req.responseText;
        }
      });
    
      const formData = new FormData(formElem);
      
      req.open(formElem.method, formElem.action);
      req.send(formData);
    }
    
    formElem.addEventListener('submit', sendAndEmbedRequest);
    
    document
      .querySelector('select')
      .addEventListener('change', event => {
        sendAndEmbedRequest();
      });
    
    sendAndEmbedRequest();
    

Validierung von Formularen mit JavaScript und AJAX

HTML5 constraint validation API
Ermöglicht JavaScript den Zugriff auf Validierungseigenschaften wie bei der “normalen” Validierung
Erlaubt Validierung mit Rückgriff auf den Datenbestand
  • Ist der Benutzername schon vergeben?
  • Kann der Warenkorb überhaupt in das derzeit gewählte Land geliefert werden?
  • Darf das Benutzerkonto die entsprechende Anfrage überhaupt vornehmen?
  • ..?
In Kombination mit AJAX und modularem serverseitigen Code: Effiziente und komplexe clientseitige Validierung ohne Codeverdoppelung
  • Server muss sinnvolle Endpunkte für Validierung bereitstellen
    • Auf Basis einzelner Felder?
    • Für das gesamte Formular als Einheit?
  • Client muss einmalig zur Übermittlung und Anzeige der Validierungsdaten programmiert werden

JavaScript-Schnittstelle

Fehler melden mit der setCustomValidity(string)-Methode von HTMLInputElement
Browser zeigt die Fehlermeldung (hoffentlich) an, eine leere Meldung markiert ein Element als valide
  • javascript_browser/ajax-form-validate.html | Validieren
    <form action="/form" method="POST">
      <input type="text" name="username">
      <input type="submit">
    </form>
    
  • javascript_browser/ajax-form-validate.js
    const inputNameElem = document.querySelector('[name="username"]');
    
    inputNameElem.addEventListener('keyup', event => {
      const req = new XMLHttpRequest();
    
      req.addEventListener('loadend', _ => {
        let errorMsg = "";
        if (req.status != 200) {
          errorMsg = "Computer says no";
        }
    
        inputNameElem.setCustomValidity(errorMsg);
      });
    
      req.open("GET", `/interactive/api/validate/username/${inputNameElem.value}`);
      req.send();
    });
    

Validierung komplexer Formulare

Leider kein Patentrezept verfügbar :(
Je nach Anwendungsfall ist die Validierung von Formulardaten ein wirklich komplexes Unterfangen
Lohnenswert: Konsistentes Vorgehen für jegliche Validierung
Nicht das Rad für jedes Formular neu erfinden

Umgang mit vom Browser zwischengespeicherten Eingaben

Komfortfunktion aktueller Browser: Eingaben in Formularelementen zwischenspeichern
Sehr hilfreich wenn ein Formular abgelehnt wird und der Webentwickler es nicht für nötig befindet die eingetragenen Werte
Konkrete Implementierung abhängig vom spezifischen Browser
Der HTML-Standard schreibt dieses Verhalten nicht vor
Problem: Formulare sind möglicherweise vorausgefüllt, ohne dass die passenden Ereignisse ausgelöst wurden
Auch hier: Verhalten im Detail abhängig vom konkreten Browser
Mögliche Gegenmaßnahme: Speicherung von Werten verbieten
Attribut autocomplete auf off setzen, entweder für einzelne Elemente oder für das gesamte Formular
Bessere Gegenmaßnahme: Alle Eingabeelemente nach dem Laden einmalig verarbeiten
Callback-Funktion also nach dem Ladevorgang mit einem künstlichen Ereignis aufrufen

Beispiel für unbehandelten Umgang mit zwischengespeicherten Werten

Anzeige der Hobbyanzahl manchmal defekt, wenn die “Zurück” oder “Neuladen”-Funktion des Browsers genutzt wird
Gilt zumindest für Firefox 53, Chrome 58
  • javascript_browser/form-history-broken.html | Validieren
    <form action="/form" method="POST">
      <div>
        <label>
          <input type="text" name="list-of-hobbies"> Liste der Hobbies
        </label>
        <p>
          Du hast <span id="num-hobbies">0</span> Hobbies angegeben.
        </p>
        <input type="submit">
      </div>
    </form>
    
  • javascript_browser/form-history-broken.js
    const hobbyTextElem = document.querySelector('[name="list-of-hobbies"]');
    const hobbyCountElem = document.querySelector('#num-hobbies');
    
    hobbyTextElem.addEventListener('keyup', event => {
      const nonEmptyHobbyList = hobbyTextElem.value
            .split(",")
            .map(v => v.trim())
            .filter(v => v.length > 0);
      hobbyCountElem.textContent = nonEmptyHobbyList.length;
    });
    
    

Beispiel für expliziten Umgang mit zwischengespeicherten Werten

Code des Event-Handlers wird nun einmalig zu Beginn ausgeführt
HTML-Code ist bis auf die Einbindung des neuen JavaScript-Quelltextes identisch
  • javascript_browser/form-history.html | Validieren
    <form action="/form" method="POST">
      <div>
        <label>
          <input type="text" name="list-of-hobbies"> Liste der Hobbies
        </label>
        <p>
          Du hast <span id="num-hobbies">0</span> Hobbies angegeben.
        </p>
        <input type="submit">
      </div>
    </form>
    
  • javascript_browser/form-history.js
    const hobbyTextElem = document.querySelector('[name="list-of-hobbies"]');
    const hobbyCountElem = document.querySelector('#num-hobbies');
    
    const updateHobbyCount = event => {
      const nonEmptyHobbyList = hobbyTextElem.value
            .split(",")
            .map(v => v.trim())
            .filter(v => v.length > 0);
      hobbyCountElem.textContent = nonEmptyHobbyList.length;
    };
    
    hobbyTextElem.addEventListener('keyup', updateHobbyCount);
    updateHobbyCount();
    

Reihenfolge der Ereignisbehandlung

G div div p p div->p div->p p->div q q p->q em1 em p->em1 p->em1 em2 em p->em2 em1->p
Registrierungsreihenfolge ist (meistens) unerheblich, es zählt die DOM-Struktur
Ladereihenfolge von Skripten also ebenfalls unerheblich
Verarbeitung erfolgt in drei Phasen
  1. “Capturing Phase”, von der Wurzel bis zum Zielelement
  2. “Target Phase”, das Zielelement wurde erreicht
  3. “Bubbling Phase”, vom Zielelement bis zur Wurzel
Jedes Ereignis wird grundsätzlich für jedes Element auf dem Pfad ausgelöst
Dabei ist es irrelevant, ob eine Ereignisbehandlungsroutine registriert ist oder nicht
Standardmäßig werden Ereignisse für die “Bubbling Phase” registriert
Aufruf von addEventListener(type, callback)
Dritter Parameter von addEventListener kann auf true gesetzt werden
Dann wird das Ereignis für die Capturing Phase registriert.
Eigenschaften aller Event-Typen
  • currentTarget verweist auf das aktuelle Element, für welches die Funktion aufgerufen wurde
  • target verweist auf das endgültige Zielement in der “Target Phase”
  • eventPhase bezeichnet die Phase, in deren Kontext der Aufruf stattfindet, mögliche Werte sind Event.CAPTURING_PHASE, EVENT.AT_TARGET und EVENT.BUBBLING_PHASE

Beispiel für “Bubbling”

Beispiel für “Capturing” & “Bubbling”

Ausnahme: Überschneidung in der Target Phase
Für mehrere Ereignisbehandlungsroutinen innerhalb der gleichen Phase für das gleiche Element gilt doch die Registrierungsreihenfolge

Blockieren oder Modifizieren von Ereignissen

stopPropagation()-Methode verhindert die Weiterverbreitung
Kann in der “Capturing Phase” die “Bubbling Phase” blockieren
preventDefault()-Methode von Ereignissen verhindert die normale Behandlung durch den Browser
Voraussetzung für Veränderungen am DOM-Baum als Reaktion auf Links oder submit-Knöpfe
  • javascript_browser/event-block.html | Validieren
    <dl>
      <dt>Lord Patrician</dt>
      <dt>Lord Havelock Vetinari</dt>
      <dd>
        He is the <a href="https://en.wikipedia.org/wiki/Primus_inter_pares">
        Primus inter pares</a> of the city state of Ankh-Morpork.
      </dd>
    </dl>
    
    
  • javascript_browser/event-block.js
    const mostElements = Array.from(document.querySelectorAll('dl, dd, a'))
    
    mostElements.forEach(elem => {
        elem.addEventListener('click', event => {
          alert(event.currentTarget.tagName);
        });
    });
    
    document.querySelector('dt').addEventListener('click', event => {
      alert(event.currentTarget.tagName);
      event.stopPropagation();
    });
    
    document.querySelector('a').addEventListener('click', event => {
      event.preventDefault();
      alert('Keine Navigation!');
    });