Implement Expand Collapse in SPFx with JavaScript

3 minute read

Implement Expand Collapse in SPFx with JavaScript

Overview

Expand / collapse (aka Accordion) is a very common 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

We will develop the SPFx web part as shown below:

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

The End Result

When deployed, the web part works as below:

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.

Leave a comment