Skip to content

Accordion Refactor

Jetzt wird aufgeräumt, durchgeputzt … aus imperativem Code wird deklarativer, und wir schreiben single purpose Funktionen.

Unser Code sieht aktuell so aus:

const accordionContainer = document.querySelector('.accordion-container');
accordionContainer.addEventListener('click', e => {
const accordionHeader = e.target.closest('.accordion-header');
if (!accordionHeader) return;
const accordion = accordionHeader.parentElement;
// we need this part to calculate the height:
const accordionContent = accordionHeader.nextElementSibling;
const accordionInner = accordionContent.children[0];
const height = accordion.classList.contains('is-open')
? 0
: accordionInner.getBoundingClientRect().height;
///
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
})

Im ersten Schritt wollen wir eine Funktion zur Ermittlung der Höhe schreiben. Eine gute Bezeichnung ist function getContentHeight() – sie beschreibt den Zweck der Funktion genau. Wir packen alles, was zur Ermittlung der Höhe dazugehört, in diese Funktion:

function getContentHeight() {
const accordionContent = accordionHeader.nextElementSibling;
const accordionInner = accordionContent.children[0];
const height = accordion.classList.contains('is-open')
? 0
: accordionInner.getBoundingClientRect().height;
}

Wenn wir die Funktion getContentHeight() aufrufen, möchten wir, dass sie uns den Wert der Höhe zurückgibt – das ist der Zweck der Funktion. Wir fügen also ein return ein:

function getContentHeight() {
// ...
return accordion.classList.contains('is-open')
? 0
: accordionInner.getBoundingClientRect().height;
}

getContentHeight() benötigt zwei Variable: accordionHeader und accordion. accordionHeader benötigen wir, um accordionContent und accordionInner zu erhalten, und accordion wird benötigt, um zu prüfen, ob das Accordeon geöffnet ist. Wir könnten jetzt beide Variable als Argumente übergeben …

function getContentHeight(accordion, accordionHeader) {
// ...
}

… aber bei genauer Betrachtung sehen wir, dass wir accordionHeader auch aus accordion direkt erhalten, ohne Umweg über accordionHeader und accordionContent:

function getContentHeight(accordion) {
const accordionInner = accordion.querySelector('.accordion-inner');
return accordion.classList.contains('is-open')
? 0
: accordionInner.getBoundingClientRect().height;
}

In diesem Fall ist ein if Statement deutlich besser lesbar als ein ternary Operator:

function getContentHeight(accordion) {
const accordionInner = accordion.querySelector('.accordion-inner');
if (accordion.classList.contains('is-open')) return 0;
return accordionInner.getBoundingClientRect().height;
}

Zum Schluss wollen wir die Funktion noch kommentieren:

/**
* Returns the height of the accordion content
* @param {HTMLElement} accordion The accordion
* @returns {Number} The accordion content's height in px
*/
function getContentHeight(accordion) {
// ...
}

Unser Code mit dem Aufruf der Funktion im EventListener sieht nun so aus:

const accordionContainer = document.querySelector('.accordion-container');
/**
* Returns the height of the accordion content
* @param {HTMLElement} accordion The accordion
* @returns {Number} The accordion content's height in px
*/
function getContentHeight(accordion) {
const accordionInner = accordion.querySelector('.accordion-inner');
if (accordion.classList.contains('is-open')) return 0;
return accordionInner.getBoundingClientRect().height;
}
accordionContainer.addEventListener('click', e => {
const accordionHeader = e.target.closest('.accordion-header');
if (!accordionHeader) return;
const accordion = accordionHeader.parentElement;
// function call:
const height = getContentHeight(accordion);
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
});

Auch das eigentliche Update des Accordeon-Status (das sind die letzten beiden Zeilen im Code) können wir in eine Funktion auslagern: function updateAccordion().

accordionContainer.addEventListener('click', e => {
// ...
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
});

wird zu

function updateAccordion() {
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
}

Welche Werte brauchen wir dafür?

  • accordion
  • accordionContent
  • height

Wir brauchen aber nicht alle drei Variable, weil wir accordionContent aus accordion rauslesen können:

function updateAccordion(accordion) {
const accordionContent = accordion.querySelector('.accordion-content');
// Updates the accordion
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
}

Was ist mit height? Wir könnten die Höhe mit der Funktion getContentHeight ermitteln:

function updateAccordion(accordion) {
const height = getContentHeight(accordion);
const accordionContent = accordion.querySelector('.accordion-content');
// Updates the accordion
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
}

Das wollen wir aber nicht, weil die Funktion updateAccordion() nur eine Sache machen soll, nämlich das Accordeon updaten. Daher übergeben wir die Höhe der Funktion als Argument:

function updateAccordion(accordion, height) {
const accordionContent = accordion.querySelector('.accordion-content');
// Updates the accordion
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
}

Und so wird die Funktion im Event-Listener verwendet:

accordionContainer.addEventListener('click', e => {
const accordionHeader = e.target.closest('.accordion-header');
if (!accordionHeader) return;
const accordion = accordionHeader.parentElement;
// function call:
const height = getContentHeight(accordion);
// Updating the accordion
updateAccordion(accordion, height);
});

In einem letzten Schritt können wir unsere Funktionen ganz nach oben schieben, da keine davon Variablen aus dem global scope verwendet:

Hier der vollständige Code:

/**
* Finds the correct height of the accordion content
* @param {HTML-Element} accordion
* @returns {Number} accordion content's height in px
*/
function getContentHeight(accordion) {
const accordionInner = accordion.querySelector('.accordion-inner');
if (accordion.classList.contains('is-open')) return 0;
return accordionInner.getBoundingClientRect().height;
}
/**
* Updates the accordion
* @param {HTML-Element} accordion
* @returns {}
*/
function updateAccordion(accordion, height) {
const accordionContent = accordion.querySelector('.accordion-content');
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;
}
const accordionContainer = document.querySelector('.accordion-container');
accordionContainer.addEventListener('click', e => {
const accordionHeader = e.target.closest('.accordion-header');
if (!accordionHeader) return;
const accordion = accordionHeader.parentElement;
const accordionContent = accordionHeader.nextElementSibling;
const height = getContentHeight(accordion);
updateAccordion(accordion, height);
});