Leihinstrumente der Musikschule

Filtere die angezeigten Instrumente nach Instrumentenart und maximaler Leihgebühr.


Erklärungen zum Code

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.


Grundidee: Was macht die Web Component?

So bleibt der Code für die Instrumentenliste schön gekapselt und kann bei Bedarf mehrfach verwendet werden.


HTML-Struktur außerhalb der Komponente

Im <body> der Seite steht:

<h1>Leihinstrumente der Musikschule</h1>
<p></p>

<instrumenten-liste data-src="./js/instrumente.json"></instrumenten-liste>

Die Klasse InstrumentenListe

class InstrumentenListe extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._allInstruments = [];
    this._selectedTypes = new Set();
    this._selectedMaxFee = 'alle';
}

Startpunkt: connectedCallback

connectedCallback() {
  const src = this.getAttribute('data-src');
  if (!src) { ... }
  this._renderSkeleton();
  this._loadData(src);
}

HTML-Layout im Shadow DOM

_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>
  `;
}

Daten laden und sortieren

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();
}

Filter für Instrumentenart (Checkboxen, UND-Verknüpfung)

Typen aus den Daten sammeln

const types = new Set();
this._allInstruments.forEach(instr => {
  (instr.Beschreibung || []).forEach(tag => types.add(tag));
});

Checkbox-Elemente erzeugen

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);
  }
});

Änderungen an den Checkboxen verfolgen

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();
  });
});

Filter für Leihgebühr (Radio-Buttons)

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();
    }
  });
});

Die eigentliche Filterlogik in _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;
  });

Ergebnisse anzeigen

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);
});

Registrierung des Custom Elements

customElements.define('instrumenten-liste', InstrumentenListe);