Filtere die angezeigten Instrumente nach Instrumentenart und maximaler Leihgebühr.
Ziel bei diesem Beispiel war es, eine Liste mit Leihinstrumenten auf diversen Seiten einer Musikschule
anzeigen zu lassen.
Die Liste lässt sich mit der Web-Component <instrumenten-liste
data-src="./js/instrumente.json"></instrumenten-liste> an beliebigen Stellen des
Webauftritts einbauen. Die Leihinstrumente werden in einer externen JSON-Datei gespeichert.
Im nachfolgenden eine Erläuterung dieses Beispiels. Schau dir dazu gerne den Quellcode dieser Datei an.
Der Code macht im Kern drei Dinge: Er lädt eine JSON-Datei mit Instrumenten, sortiert und filtert die Daten
in
JavaScript und zeigt das Ergebnis über eine Web Component im Browser an.
<instrumenten-liste>. instrumente.json So bleibt der Code für die Instrumentenliste schön gekapselt und kann bei Bedarf mehrfach verwendet werden.
Im <body> der Seite steht:
<h1>Leihinstrumente der Musikschule</h1>
<p>…</p>
<instrumenten-liste data-src="./js/instrumente.json"></instrumenten-liste>
<instrumenten-liste> ist der Platzhalter für unsere Web Component. data-src="./js/instrumente.json" sagt der Komponente, wo die Daten
liegen.
InstrumentenListeclass InstrumentenListe extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._allInstruments = [];
this._selectedTypes = new Set();
this._selectedMaxFee = 'alle';
}
extends HTMLElement: Die Klasse ist ein eigenes HTML-Element. constructor():super() ruft den Konstruktor von HTMLElement auf (Pflicht). attachShadow({ mode: 'open' }) erzeugt ein Shadow DOM – ein „inneres DOM“,
das
von außen gekapselt ist. _allInstruments: Array mit allen Instrumenten aus der JSON-Datei. _selectedTypes: Menge (Set) der gerade ausgewählten Beschreibungstypen („für
Anfänger“,
„Gitarre“ etc.). _selectedMaxFee: aktuell ausgewählte maximale Leihgebühr („alle“,
20,
40, 60).connectedCallbackconnectedCallback() {
const src = this.getAttribute('data-src');
if (!src) { ... }
this._renderSkeleton();
this._loadData(src);
}
connectedCallback() wird automatisch aufgerufen, sobald das Element im DOM hängt. data-src, baut die Grundstruktur (Filter-Bereich + Ergebnisliste) und
lädt danach die Daten. _renderSkeleton() {
this.shadowRoot.innerHTML = `
<div class="layout">
<section class="filters">
<!-- Instrumentenart-Filter -->
<fieldset>...</fieldset>
<!-- Leihgebühr-Filter -->
<fieldset>...</fieldset>
</section>
<section>
<div class="instrument-list" id="list"></div>
<p class="no-results" id="no-results" hidden>...</p>
</section>
</div>
`;
}
#type-filters: Hier werden die Checkboxen für die Beschreibungstypen später
eingefügt.
#fee-filters: Enthält die Radio-Buttons für die maximale
Leihgebühr.
#list: Hier werden die Instrumentkarten angezeigt. #no-results: Meldung, falls kein Instrument zum aktuellen Filter passt.async _loadData(src) {
const res = await fetch(src);
const data = await res.json();
data.sort((a, b) => a.name.localeCompare(b.name, 'de'));
this._allInstruments = data;
this._setupFilters();
this._updateList();
}
fetch(src) lädt die JSON-Datei vom Server und res.json() macht daraus ein
JavaScript-Array. sort(...localeCompare...) sortiert die Instrumente alphabetisch nach name
(z.B.
A–Z nach deutschem Alphabet). _setupFilters() baut die Filter (Checkboxen/Radio-Buttons) auf. _updateList() berechnet und zeigt die erste Liste (mit Startfiltern).const types = new Set();
this._allInstruments.forEach(instr => {
(instr.Beschreibung || []).forEach(tag => types.add(tag));
});
Beschreibung-Arrays der Instrumente werden alle vorkommenden Wörter eingesammelt
(z.B. „Gitarre“, „für Anfänger“). Set sorgt dafür, dass jeder Typ nur einmal vorkommt.const typeContainer = this.shadowRoot.querySelector('#type-filters');
const INITIAL_TYPE = 'für Anfänger';
types.forEach(type => {
const id = 'type-' + type.toLowerCase().replace(/\s+/g, '-');
const isInitial = type === INITIAL_TYPE;
const wrapper = document.createElement('label');
wrapper.innerHTML = `
<input type="checkbox" value="${type}" id="${id}" ${isInitial ? 'checked' : ''}>
${type}
`;
typeContainer.appendChild(wrapper);
if (isInitial) {
this._selectedTypes.add(type);
}
});
<label> mit einer Checkbox erzeugt. _selectedTypes eingetragen.
const typeInputs = this.shadowRoot.querySelectorAll('#type-filters input[type="checkbox"]');
typeInputs.forEach(input => {
input.addEventListener('change', () => {
const val = input.value;
if (input.checked) {
this._selectedTypes.add(val);
} else {
this._selectedTypes.delete(val);
}
this._updateList();
});
});
checked = true kommt der Typ in _selectedTypes. checked = false wird der Typ entfernt. _updateList() aufgerufen, um die angezeigten Instrumente neu zu berechnen.
const feeInputs = this.shadowRoot.querySelectorAll('#fee-filters input[type="radio"]');
feeInputs.forEach(input => {
input.addEventListener('change', () => {
if (input.checked) {
this._selectedMaxFee = input.value; // "alle" oder Zahl als String
this._updateList();
}
});
});
name="fee", daher kann immer nur einer
aktiv
sein. value ist entweder "alle" oder eine Zahl als String
("20", "40", "60"). _selectedMaxFee gesetzt und _updateList() neu
ausgeführt. _updateList()
_updateList() {
const listEl = this.shadowRoot.querySelector('#list');
const noResultsEl = this.shadowRoot.querySelector('#no-results');
listEl.innerHTML = '';
const requiredTypes = Array.from(this._selectedTypes);
const requireTypeMatch = requiredTypes.length > 0;
const filtered = this._allInstruments.filter(instr => {
const beschreibung = instr.Beschreibung || [];
// Typen-Filter (UND-Logik)
const typeMatch = !requireTypeMatch ||
requiredTypes.every(tag => beschreibung.includes(tag));
// Gebühren-Filter (Radio)
const fee = Number(instr.Leihgebühr);
let feeMatch = true;
if (this._selectedMaxFee !== 'alle') {
const max = Number(this._selectedMaxFee);
feeMatch = fee <= max;
}
return typeMatch && feeMatch;
});
requiredTypes: Array aller aktuell angehakten Beschreibungstypen. requireTypeMatch: Nur wenn mindestens ein Typ gewählt ist, muss darauf gefiltert werden.
filter(...) läuft über alle Instrumente und prüft:
Typen (UND-Verknüpfung)
requiredTypes.every(tag => beschreibung.includes(tag))
Das bedeutet: Für dieses Instrument müssen wirklich alle gewählten Typen in
Beschreibung vorkommen (z.B. sowohl „Gitarre“ als auch „für Fortgeschrittene“).
Leihgebühr (Radio)
_selectedMaxFee === 'alle': keine Einschränkung. fee <= max (z.B. bis 40 €). Nur wenn beide Bedingungen wahr sind, bleibt das Instrument im filtered-Array.
if (filtered.length === 0) {
noResultsEl.hidden = false;
return;
}
noResultsEl.hidden = true;
filtered.forEach(instr => {
const item = document.createElement('article');
item.className = 'instrument';
const tags = (instr.Beschreibung || [])
.map(t => `<span class="tag">${t}</span>`)
.join('');
item.innerHTML = `
<h3>${instr.name}</h3>
<div class="meta">Leihgebühr: ${instr.Leihgebühr} € / Monat</div>
<div class="tags">${tags}</div>
`;
listEl.appendChild(item);
});
<article> mit Titel, Leihgebühr und einer
Liste
von <span class="tag">Beschreibung</span>. <span>-Elemente werden durch CSS mit Abstand und Hintergrund versehen,
damit
die Beschreibungen wie kleine „Chips“ aussehen. customElements.define('instrumenten-liste', InstrumentenListe);
<instrumenten-liste> vorkommt, soll das
mit
der Klasse InstrumentenListe verbunden werden. <div> oder
<section>.