Simple Accordion For SPFx With JavaScript

Overview

Accordion is a very frequent requirement in SPFx web parts. There are various npm packages available for implementing this.

In this article, we will explore implementing expand / collapse functionality in SPFx using plain JavaScript. Although the implementation is with plain JavaScript, it can also be used with React based implementation.

Develop SPFx Solution

Let us develop the SPFx web part as shown below,

Simple Accordion for SPFx with JavaScript

In the solution, create a model at src\webparts\expandCollapse\IAnnouncementItem.ts to represent an Announcement item.

export interface IAnnouncementItem {
    title: string;
    description: string;
}

In the web part, implement a method to read announcements. For simplicity, I am using mock method to return dummy announcements.

public getAnnouncementItems(): IAnnouncementItem[] {
    const announcementItems: IAnnouncementItem[] = [
      { title: "What is a product key?", description: "A product key is a 25-character code that comes with a Microsoft Office product. The product key allows you to install and activate the Office product on your PC." },
      { title: "Where do I find my Office product key?", description: "Your product key is 25 characters and is found in different locations depending on how you acquired your Office product." },
      { title: "How long does it take to download?", description: "Download times vary by location, internet connection speed and the size of the Office product you are downloading. It is recommended only high-speed broadband connections are used to download your file(s)." },
      { title: "What happens after I download?", description: "After the download has completed, go to the location that you saved the file at and double click on the new icon to start the installation." },
      { title: "What if the download stops or is interrupted before it is complete?", description: "If you become disconnected while files are being downloaded through your web browser, reconnect to the internet and retry your download." }
    ];

    return announcementItems;
}

The above FAQs are taken from Microsoft Frequently Asked Questions page.

Update the render method to display the announcements.

public render(): void {
    this.enableExpandCollapse();

    this.domElement.innerHTML = `
      <div class="${styles.expandCollapse}">
        <div class="${styles.container}">
          <div class="${styles.row}">
            <div class="${styles.justifyContentEnd}">
              <a href="#" id="linkExpandAll" style="display: none" rel="noreferrer" onClick="${() => { this.expandAll(); }}">Expand all</a>
              <a href="#" id="linkCollapseAll" rel="noreferrer" onClick="${() => { this.collapseAll(); }}">Collapse all</a>
            </div>

            <div id="tblAnnouncementDetails">
            </div>            
            
          </div>
        </div>
      </div>`;

    this.getAnnouncementDetails();
    this._setButtonEventHandlers();
}

In the enableExpandCollapse method, we use setInterval to evaluate the existence of an element with className collapsible and add click event to an individual button to expand/collapse a specific div next to it.

private enableExpandCollapse() {
    const existCondition = setInterval(() => {
      if (document.getElementsByClassName("collapsible").length > 0) {
        const coll = document.getElementsByClassName("collapsible");

        for (let i = 0; i < coll.length; i++) {
          coll[i].addEventListener("click", function () {
            this.classList.toggle("active");
            const content = this.parentElement.nextElementSibling;
            if (content.style.display === "block" || content.style.display === "") {
              this.textContent = "▼";
              content.style.display = "none";
            } else {
              this.textContent = "▲";
              content.style.display = "block";
            }
          });
        }
        clearInterval(existCondition);
      }
    }, 100);
}

The getAnnouncementDetails method will return the HTML structure for announcements to render.

private getAnnouncementDetails() {
    const announcementItems: IAnnouncementItem[] = this.getAnnouncementItems();

    let html: string = "<div>";

    announcementItems.forEach((item: IAnnouncementItem) => {
      html += `
      <div class="${styles.announcementItem}">
        <div class="${styles.titleRow}">
          <label>${item.title}</label>
          <button type="button" class="${styles.buttonExpandCollapse} collapsible"}>▲</button>
        </div>
        <div class="${styles.descriptionRow}">
          <label>${item.description}</label>
        </div>
      </div>`;
    });

    html += '</div>';

    const announcementContainer: Element = this.domElement.querySelector('#tblAnnouncementDetails');
    announcementContainer.innerHTML = html;
}

The _setButtonEventHandlers method will register the event handlers for Expand/collapse buttons,

private _setButtonEventHandlers(): void {
    const webPart: ExpandCollapseWebPart = this;
    this.domElement.querySelector('#linkExpandAll').addEventListener('click', () => { webPart.expandAll(); });
    this.domElement.querySelector('#linkCollapseAll').addEventListener('click', () => { webPart.collapseAll(); });
}

In the expandAll method, we are expanding all the divs using CSS style of display block, as clicked.

private expandAll() {
    if (document.getElementsByClassName("collapsible").length > 0) {
      const coll = document.getElementsByClassName("collapsible");

      for (let i = 0; i < coll.length; i++) {
        const content: any = coll[i].parentElement.nextElementSibling;
        coll[i].textContent = "▲";
        content.style.display = "block";
      }
    }

    const linkCollapseAll = document.getElementById("linkCollapseAll");
    const linkExpandAll = document.getElementById("linkExpandAll");
    if (typeof linkCollapseAll !== "undefined" && typeof linkExpandAll !== "undefined") {
      linkCollapseAll.style.display = "block";
      linkExpandAll.style.display = "none";
    }
}

In the collapseAll method, we are collapsing all the divs using CSS style of display none, as clicked.

private collapseAll() {
    if (document.getElementsByClassName("collapsible").length > 0) {
      const coll = document.getElementsByClassName("collapsible");

      for (let i = 0; i < coll.length; i++) {
        const content: any = coll[i].parentElement.nextElementSibling;
        coll[i].textContent = "▼";
        content.style.display = "none";
      }
    }

    const linkCollapseAll = document.getElementById("linkCollapseAll");
    const linkExpandAll = document.getElementById("linkExpandAll");
    if (typeof linkCollapseAll !== "undefined" && typeof linkExpandAll !== "undefined") {
      linkCollapseAll.style.display = "none";
      linkExpandAll.style.display = "block";
    }
}

Accordion in an action

When deployed, the web part works as below:

Simple Accordion for SPFx with JavaScript

Summary

A common requirement of expand/collapse (Accordion) functionality can be implemented using plain JavaScript in SPFx. As well, there are various third party npm packages available for this functionality.

Code Download

The SPFx code developed for this article can be found here.