How to Animate the Details Element Using WAAPI

Avatar of Louis Hoebregts
Louis Hoebregts am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Das Animieren von Akkordeons in JavaScript war eine der am häufigsten nachgefragten Animationen auf Websites. Fun Fact: jQuerys slideDown()-Funktion war bereits in der ersten Version im Jahr 2006 verfügbar.

In diesem Artikel sehen wir uns an, wie Sie das native <details>-Element mithilfe der Web Animations API animieren können.

HTML-Setup

Zuerst sehen wir uns an, wie wir das Markup für diese Animation strukturieren.

Das <details>-Element benötigt ein <summary>-Element. Die Zusammenfassung ist der Inhalt, der sichtbar ist, wenn das Akkordeon geschlossen ist.
Alle anderen Elemente innerhalb von <details> sind Teil des inneren Inhalts des Akkordeons. Um die Animation dieses Inhalts zu erleichtern, verpacken wir ihn in ein <div>.

<details>
  <summary>Summary of the accordion</summary>
  <div class="content">
    <p>
      Lorem, ipsum dolor sit amet consectetur adipisicing elit.
      Modi unde, ex rem voluptates autem aliquid veniam quis temporibus repudiandae illo, nostrum, pariatur quae!
      At animi modi dignissimos corrupti placeat voluptatum!
    </p>
  </div>
</details>

Accordion-Klasse

Um unseren Code wiederverwendbarer zu machen, sollten wir eine Accordion-Klasse erstellen. Auf diese Weise können wir new Accordion() für jedes <details>-Element auf der Seite aufrufen.

class Accordion {
  // The default constructor for each accordion
  constructor() {}

  // Function called when user clicks on the summary
  onClick() {}

  // Function called to close the content with an animation
  shrink() {}

  // Function called to open the element after click
  open() {}

  // Function called to expand the content with an animation
  expand() {}

  // Callback when the shrink or expand animations are done
  onAnimationFinish() {}
}

Constructor()

Der Konstruktor ist der Ort, an dem wir alle pro Akkordeon benötigten Daten speichern.

constructor(el) {
  // Store the <details> element
  this.el = el;
  // Store the <summary> element
  this.summary = el.querySelector('summary');
  // Store the <div class="content"> element
  this.content = el.querySelector('.content');

  // Store the animation object (so we can cancel it, if needed)
  this.animation = null;
  // Store if the element is closing
  this.isClosing = false;
  // Store if the element is expanding
  this.isExpanding = false;
  // Detect user clicks on the summary element
  this.summary.addEventListener('click', (e) => this.onClick(e));
}

onClick()

In der onClick()-Funktion prüfen wir, ob das Element animiert wird (schließen oder öffnen). Dies müssen wir tun, falls Benutzer auf das Akkordeon klicken, während es animiert wird. Bei schnellen Klicks möchten wir nicht, dass das Akkordeon von vollständig geöffnet zu vollständig geschlossen springt.

Das <details>-Element hat ein Attribut, [open], das vom Browser darauf angewendet wird, wenn wir das Element öffnen. Wir können den Wert dieses Attributs abrufen, indem wir die open-Eigenschaft unseres Elements mit this.el.open überprüfen.

onClick(e) {
  // Stop default behaviour from the browser
  e.preventDefault();
  // Add an overflow on the <details> to avoid content overflowing
  this.el.style.overflow = 'hidden';
  // Check if the element is being closed or is already closed
  if (this.isClosing || !this.el.open) {
    this.open();
  // Check if the element is being openned or is already open
  } else if (this.isExpanding || this.el.open) {
    this.shrink();
  }
}

shrink()

Diese shrink-Funktion verwendet die WAAPI-Funktion .animate(). Sie können mehr darüber in der MDN-Dokumentation lesen. WAAPI ist sehr ähnlich zu CSS @keyframes. Wir müssen die Start- und End-Keyframes der Animation definieren. In diesem Fall benötigen wir nur zwei Keyframes, der erste ist die aktuelle Höhe des Elements und der zweite ist die Höhe des <details>-Elements, sobald es geschlossen ist. Die aktuelle Höhe wird in der Variablen startHeight gespeichert. Die geschlossene Höhe wird in der Variablen endHeight gespeichert und entspricht der Höhe des <summary>.

shrink() {
  // Set the element as "being closed"
  this.isClosing = true;

  // Store the current height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the height of the summary
  const endHeight = `${this.summary.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow or fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });

  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(false);
  // If the animation is cancelled, isClosing variable is set to false
  this.animation.oncancel = () => this.isClosing = false;
}

open()

Die open-Funktion wird aufgerufen, wenn wir das Akkordeon erweitern möchten. Diese Funktion steuert die Animation des Akkordeons noch nicht. Zuerst berechnen wir die Höhe des <details>-Elements und wenden diese Höhe mit Inline-Stilen darauf an. Sobald dies geschehen ist, können wir das `open`-Attribut darauf setzen, um den Inhalt sichtbar zu machen, aber ihn zu verstecken, da wir `overflow: hidden` und eine feste Höhe für das Element haben. Dann warten wir auf den nächsten Frame, um die expand-Funktion aufzurufen und das Element zu animieren.

open() {
  // Apply a fixed height on the element
  this.el.style.height = `${this.el.offsetHeight}px`;
  // Force the [open] attribute on the details element
  this.el.open = true;
  // Wait for the next frame to call the expand function
  window.requestAnimationFrame(() => this.expand());
}

expand()

Die expand-Funktion ist ähnlich wie die shrink-Funktion, aber anstatt von der aktuellen Höhe zur geschlossenen Höhe zu animieren, animieren wir von der Höhe des Elements zur Endhöhe. Diese Endhöhe entspricht der Höhe der Zusammenfassung plus der Höhe des inneren Inhalts.

expand() {
  // Set the element as "being expanding"
  this.isExpanding = true;
  // Get the current fixed height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the open height of the element (summary height + content height)
  const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow of fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });
  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(true);
  // If the animation is cancelled, isExpanding variable is set to false
  this.animation.oncancel = () => this.isExpanding = false;
}

onAnimationFinish()

Diese Funktion wird am Ende sowohl der schrumpfenden als auch der erweiternden Animation aufgerufen. Wie Sie sehen können, gibt es einen Parameter, [open], der auf `true` gesetzt wird, wenn das Akkordeon geöffnet ist. Dies ermöglicht es uns, das [open] HTML-Attribut auf das Element zu setzen, da es nicht mehr vom Browser gehandhabt wird.

onAnimationFinish(open) {
  // Set the open attribute based on the parameter
  this.el.open = open;
  // Clear the stored animation
  this.animation = null;
  // Reset isClosing & isExpanding
  this.isClosing = false;
  this.isExpanding = false;
  // Remove the overflow hidden and the fixed height
  this.el.style.height = this.el.style.overflow = '';
}

Die Akkordeons einrichten

Puh, den größten Teil des Codes haben wir hinter uns!

Alles, was noch zu tun ist, ist, unsere Accordion-Klasse für jedes <details>-Element im HTML zu verwenden. Dazu verwenden wir querySelectorAll auf dem <details>-Tag und erstellen für jedes eine neue Accordion-Instanz.

document.querySelectorAll('details').forEach((el) => {
  new Accordion(el);
});

Notizen

Um die Berechnungen für die geschlossene Höhe und die geöffnete Höhe durchzuführen, müssen wir sicherstellen, dass die <summary> und der Inhalt immer die gleiche Höhe haben.

Versuchen Sie zum Beispiel nicht, dem `summary` ein Padding hinzuzufügen, wenn es geöffnet ist, da dies zu Sprüngen während der Animation führen kann. Dasselbe gilt für den inneren Inhalt – er sollte eine feste Höhe haben und wir sollten es vermeiden, Inhalte zu haben, die ihre Höhe während der Öffnungsanimation ändern könnten.

Fügen Sie auch keinen Rand zwischen der Zusammenfassung und dem Inhalt hinzu, da dieser für die Keyframes der Höhen nicht berechnet wird. Verwenden Sie stattdessen ein Padding direkt auf dem Inhalt, um etwas Abstand zu schaffen.

Das Ende

Und *voilà*, wir haben ein schönes animiertes Akkordeon in JavaScript ohne eine Bibliothek! 🌈