HTML
-Dokumente auch ohne Server erzeugenCSS
HTML
-Code generiert werden?HTML
-Strukturen natürlich dennoch auf dem Client technisch gesehen möglichHTML
-Code sollte auf Basis einer Templating-Mechanik erfolgenHTML
-ErzeugungCSS
aus.
DOM
-Baum können über allgemeine oder spezielle Ereignisse berichtenDOM
-Klassen nachgeschlagen werdenDOM
-BaumHTML
-Element steht eine korrespondierendes JavaScript-Objekt bereitquerySelector
& querySelectorAll
: Erlauben die Navigation des DOM
mit CSS
-SelektorenquerySelectorAll
liefert kein klassisches Array zurückArray.from()
notwendig um Iterationsfunktionen (außer forEach
) zu nutzenDOM
-Elementedocument.querySelector
ausreichendon
-Attribut des Elementskeydown
& keyup
: Eine Taste wurde gedrückt bzw. losgelassenkeypress
: Eine Taste wurde gedrückt und losgelassenchange
: Der Wert eines Eingabeelements hat sich geändertHTML
-Quelltext soll Strukturen widerspiegeln, nicht Quelltext beinhaltenthis.value
bei jedem Tastendruckthis
steht im Kontext dieses Ereignisses für das <input>
-Elementthis.value
ist der gleiche Wert, wie er mit <input value="xyz">
angegeben werden würde<label for="combination">
Wutbox
</label>
<input id="combination"
type="text"
onkeyup="this.value = this.value.toUpperCase();">
<script>
<script>
-BlockHTML
-Parser die entsprechende Stelle erreicht<script>
-lokalen Variablen<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>
<script>
<script>
-Block mit src
-AttributHTML
-Parser die entsprechende Stelle erreicht<script src="embed-script-src-1.js"></script>
<h1>Static Content</h1>
<script src="embed-script-src-2.js"></script>
let h1 = document.querySelector('h1');
if (h1) {
h1.textContent = '#1';
} else {
console.log('#1: Keine Überschrift gefunden');
}
// Auch hier: Deklaration von oben noch gültig
h1 = document.querySelector('h1');
if (h1) {
h1.textContent = '#2';
} else {
console.log('#2: Keine Überschrift gefunden');
}
defer
: Verschiebt Ausführung bis das Dokument vollständig geladen istasync
: Erlaubt Ladevorgang während des Parsenstype
mit dem Wert module
: Lädt referenzierte Dokumente nachHTML
-Quelltext erwähnen<noscript>
<noscript>
wird bei deaktiviertem JavaScript dargestellt<noscript>
Diese Seite benötigt JavaScript um wichtige Informationen anzuzeigen.
</noscript>
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);
});
HTML
-Quelltext zum DOM
-BaumHTML
-Quelltextes wird ein DOM
-Baum generiertHTML
-Dokuments im SpeicherHTML
-Darstellung umständlich und fehleranfälligEventTarget
: Basisklasse für alles, was ein Ereignis auslösen könnteNode
: Basisklasse für alle Knoten im DOM
-BaumDocument
: Wurzel des DOM
-BaumsElement
: Basisklasse für alle visuellen Knoten die Teil eines Dokumentes sindHTMLElement
& SVGElement
: Konkrete Klassen für HTML
- oder SVG
-KnotenHTML
-Attribute und Eigenschaften von DOM
-ObjektenCSS
- und HTML
-Eigenschaften lassen sich via JavaScript manipulierenCSS
-Eigenschaften befinden sich im Unterobjekt style
HTML
-Eigenschaften werden direkt zugewiesenclass
) aber noch besondere HilfsmethodenHTML
-Eigenschaften überschneiden sich mit CSS
, z.B. width
und height
<canvas>
und <img>
-Elementen problematisch<div>
<label>
Eingabe 1 <input type="text" id="fst">
</label>
</div>
<div>
<label>
Eingabe 2 <input type="text" id="snd">
</label>
</div>
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;
data
-Attributendata
-Namensraums für Ergänzungen von BenutzernHTML
-Quelltext möglich, Zugriff im DOM
über dataset
-Eigenschaft<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>
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;
console.log
alert
debugger
zum Setzen eines Haltepunktes<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>
const heading = document.querySelector('h1');
heading.textContent = heading.textContent.toUpperCase();
debugger;
document.querySelectorAll('li').forEach((elem, idx) => {
debugger;
elem.textContent = idx + " " + elem.textContent;
});
DOM
-BaumaddEventListener
EventTarget
und damit auf faktisch allen Objekten verfügbar
change
, click
, …)event
genanntthis
addEventListener
reagiert.
DOM
-Baumdefer
-Attribut Ausführungszeitpunkt auf nach dem Parsen festlegenDOMContentLoaded
-EreignissesDOM
geladen ist, ein bisschen besser unterstütztIn 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.
<h1>Document not yet loaded</h1>
document.addEventListener("DOMContentLoaded", function(event) {
console.log("Dokument geladen, Bestandteile sind ...");
document.querySelectorAll('*').forEach(e => console.log(e));
});
document.addEventListener("DOMContentLoaded", function(event) {
document.querySelector('h1').textContent = "Loaded!";
});
click
-EreignisMouseEvent
<ul>
{% for i in (1..5) %}
<li>Ungeklickt</li>
{% endfor %}
</ul>
const allListItems = Array.from(document.querySelectorAll('li'));
allListItems.forEach(item => {
item.addEventListener('click', function(event) {
console.log(event);
this.textContent = "Klick!";
});
});
mousein
, mouseout
und mouseover
sind ebenfalls vom Typ MouseEvent
<ul>
{% for i in (1..5) %}
<li style="color: blue;">{{ i }}</li>
{% endfor %}
</ul>
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 = "";
});
});
data
-Attributdata
-Attributendata
-Attributen schaffen<ul>
{% for i in (1..5) %}
<li data-count="0"
data-text="Anzahl Sichtungen: ">
{{ i }} hat noch nie einen Mauszeiger gesehen
</li>
{% endfor %}
</ul>
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;
});
});
classList
add(String[, String])
fügt eine variable Anzahl von Klassen hinzuremove(String[, String])
entfernt eine variable Anzahl von Klassentoggle(String)
alterniert die Existenz einer Klasse als Teil der Klassenlistetoggle(String, Boolean)
fügt die gegebene Klasse hinzu oder entfernt Sietrue
als zweiter Parameter fügt hinzu, false
entferntcontains(String)
prüft, ob die angegebene Klasse gesetzt ist<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>
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);
});
});
ul li.done {
text-decoration: line-through;
}
h1.done {
color: green;
}
value
-Eigenschaft lässt sich einfach auslesenchange
-Ereigniskeyup
-Ereignis hilfreich<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>
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;
});
DOM
-TeilbaumquerySelector
auf dem Wurzel-Element des Teilbaums aufrufen{% 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 %}
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;
});
});
filterElements
gone
von allen Elementen …<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>
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);
});
});
.gone {
display: none;
}
:not(:first-child)
<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>
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);
});
});
.gone {
display: none;
}
arrow
und function
Schreibweise: this
-Kontextthis
-Kontextarrow
-Notation von Funktionen übernimmt this
-Kontext des Elternelementstarget
-Eigenschaft des Ereignissesthis
-Kontext zugreifen möchten, nutzen Sie dafür die function
-Notation anstatt der arrow
-Notation.
AJAX
)HTML
-Fragmenten auf der SeiteXMLHttpRequest
-KlasseXML
im Namen ist irreführend, es können beliebige Daten übertragen werdenHttp
im Namen ist irreführend, es können auch andere Protokolle verwendet werdenProgressEvent
berichten über den Fortschritt einer Anfrageprogress
wird wiederholt ausgelöst, wenn sich der Fortschritt signifikant geändert hatload
wird ausgelöst, wenn der Ladevorgang erfolgreich beendet isterror
wird bei fehlgeschlagener Kommunikation ausgelöst, ein StatusCode != 200
ist kein Fehler, sondern eine Antwortabort
wird ausgelöst, wenn der Benutzer die Anfrage unterbrochen hatloadend
wird ausgelöst, wenn der Ladevorgang aus irgendeinem Grund beendet worden istopen()
und send()
um die Anfrage tatsächlich zu stellenopen(httpVerb, url)
URL
auf den exakt gleichen Host wie die ladende Seite zeigensend([data])
POST
-Anfragen zusätzlich: Angabe von Daten, die im Rumpf verschickt werden sollenAJAX
-AnfrageAJAX
-AbfrageXMLHttpRequest.statusCode
, um ausgeführte aber nicht-erfolgreiche Anfragen zu erkennen<input type="text" placeholder="URL?">
<button>AJAX-Anfrage abschicken</button>
<pre></pre>
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();
});
DOM
-TeilbäumenHTML
-Fragmente in den eigenen Quelltext einsetzenXMLHttpRequest
-Objekte verfügen nach erfolgreicher Durchführung über eine responseText
-Eigenschaft mit der Antwort des ServersDOM
-Elemente verfügen über eine innerHTML
-Eigenschaft, mit der zur Laufzeit ein neuer Teilbaum aus einem String
erzeugt werden kannevent.preventDefault()
verhindert “normale” Verarbeitung durch den Browser<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>
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();
AJAX
-AnfragenPOST
-Anfragen lassen sich vom Client einfacher parametrisierenHTML
-Dokument seinDOM
passt.hbs
-Dateien für den HTML
-Code des Fragments und eine Route, welche die entsprechenden Daten berechnetfragment
für Routenpage
, aber:
templates
-OrdnerHTML
-Formular-Semantiken aufgrund von verfügbarem JavaScript vollständig ignorierenDOM
-Teilbäumen: <form>
-Element fehlt völligvalue
-Eigenschaften arbeitenFormData
-ObjektenFormData
erlaubt den Zugriff auf alle Daten eines Formulares in exakt der Form, wie sie an den Server gesendet werden würdenFormData
erzeugen mit const formData = new FormData(formElem);
formElem
für ein mit z.B. document.querySelector()
erhaltenes FormularelementformData.get(keyString)
möglich/person/:id/name
) überschaubarer Mehraufwand nötigGET
-Anfragen mit parametriesierten URL
s oder POST
-AnfragenAJAX
-Formular mit FormData
FormData
-Instanz kann einem <form>
-DOM
-Element erzeugenXMLHttpRequest.send()
bequem an den Server übertragen werden<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>
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();
AJAX
HTML5
constraint validation APIsetCustomValidity(string)
-Methode von HTMLInputElement
<form action="/form" method="POST">
<input type="text" name="username">
<input type="submit">
</form>
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();
});
HTML
-Standard schreibt dieses Verhalten nicht vorautocomplete
auf off
setzen, entweder für einzelne Elemente oder für das gesamte Formular<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>
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;
});
HTML
-Code ist bis auf die Einbindung des neuen JavaScript-Quelltextes identisch<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>
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();
DOM
-StrukturaddEventListener(type, callback)
addEventListener
kann auf true
gesetzt werdenCapturing Phase
registriert.Event
-TypencurrentTarget
verweist auf das aktuelle Element, für welches die Funktion aufgerufen wurdetarget
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
<p>
The Patrician rules the city, and operates a specialised form of
<q>One Man, One Vote</q> democracy: the Patrician is <em>the</em>
Man, and he has <em>the</em> Vote.
</p>
document.querySelector('p').addEventListener('click', event => {
alert("Paragraph");
});
document.querySelector('q').addEventListener('click', event => {
alert("Quote");
});
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
Array.from(document.querySelectorAll('*')).forEach(elem => {
elem.addEventListener('click', e => {
alert(`Bubbling: ${elem.tagName}`);
});
elem.addEventListener('click', e => {
alert(`Capturing: ${elem.tagName}`);
}, true);
});
stopPropagation()
-Methode verhindert die WeiterverbreitungpreventDefault()
-Methode von Ereignissen verhindert die normale Behandlung durch den BrowserDOM
-Baum als Reaktion auf Links oder submit
-Knöpfe<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>
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!');
});