Project recap: FAQ Accordions

Project recap: FAQ Accordions

When we worked on an FAQ-based accordion project previously, we were dealing with a single FAQ because we didn't know about "for" loops back then.

But now, we do know a lot more than "for" loops.

So, it's time to build a realistic accordion-based FAQ section of a webpage with multiple FAQs.

Ready?

First, download the project starter files for this project and open them up inside your code editor:

If you open up the index.html file, it contains markup related to four FAQs:

<div class="faq faq-1">
    <h3 class="faq-header"><button>...</button></h3>
    <div class="faq-content">
        ...
    </div>
</div>
<div class="faq faq-2">
    <h3 class="faq-header"><button>...</button></h3>
    <div class="faq-content">
        ...
    </div>
</div>
<div class="faq faq-3">
    <h3 class="faq-header"><button>...</button></h3>
    <div class="faq-content">
        ...
    </div>
</div>
<div class="faq faq-4">
    <h3 class="faq-header"><button>...</button></h3>
    <div class="faq-content">
        ...
    </div>
</div>

If you notice, each FAQ has a parent container with the class of faq and a unique class, such as, faq-1, faq-2, etc.

Inside each FAQ's parent container, there is a headline (.faq-header) and the content (.faq-content).

And here is how FAQs look in the browser with some good styling from the style.css file:

All the FAQs currently look closed because we added the following CSS to the FAQ's content (inside the style.css file):

.faq-content {
    visibility: hidden;
    position: absolute;
    left: -9999px;
    padding: 0 30px 30px;
}

The above CSS rule hides the content of all FAQs as their initial state.

And in order to reveal an FAQ's content, all we have to do is add the class is-open to its direct parent container:

Adding the is-open class to the individual FAQ's parent container will reveal the content inside it because of the following CSS:

.faq.is-open .faq-content {
    visibility: visible;
    position: static;
}

That's how we are implementing the accordion functionality.

Long story short, if the parent container of an FAQ contains the class is-open, we are revealing its content. If not, we are hiding its content.

Anyway, we shouldn't be adding the is-open manually like above.

We should add the is-open class automatically only when someone clicks on a particular FAQ's headline.

This way, when we click on an FAQ, it expands to reveal its content automatically.

Come on, let's see how to achieve this using Javascript.

As you already know, one way to achieve it is by using the following code without using a "for" loop:

/* FAQ 1 */
const faq_1 = document.querySelector(".faq-1");
const faq_1_header = document.querySelector(".faq-1 .faq-header");

faq_1_header.addEventListener("click", function(){
    faq_1.classList.toggle("is-open");
});

/* FAQ 2 */
const faq_2 = document.querySelector(".faq-2");
const faq_2_header = document.querySelector(".faq-2 .faq-header");

faq_2_header.addEventListener("click", function(){
    faq_2.classList.toggle("is-open");
});

/* FAQ 3 */
const faq_3 = document.querySelector(".faq-3");
const faq_3_header = document.querySelector(".faq-3 .faq-header");

faq_3_header.addEventListener("click", function(){
    faq_3.classList.toggle("is-open");
});

/* FAQ 4 */
const faq_4 = document.querySelector(".faq-4");
const faq_4_header = document.querySelector(".faq-4 .faq-header");

faq_4_header.addEventListener("click", function(){
    faq_4.classList.toggle("is-open");
});

You already know what's happening in the above code because you have worked on turning an FAQ into an accordion.

But just to be on the same page...

First, we are selecting the first FAQ's parent container:

const faq_1 = document.querySelector(".faq-1");

Next, we are selecting the headline element inside it:

const faq_1_header = document.querySelector(".faq-1 .faq-header");

This results in Javascript selecting the following headline element inside the .faq-1 element:

Then, we are adding the "click" event listener to the headline element:

faq_1_header.addEventListener("click", function(){
    faq_1.classList.toggle("is-open");
});

This way, when someone clicks on FAQ 1's headline, we are toggling the class "is-open" on its parent element:

faq_1.classList.toggle("is-open");

Initially, all the FAQs are in their closed state, and the is-open class is not present on them.

So, when the first FAQ's headline is clicked for the first time, the is-open class will get added to the first FAQ's parent container:

<div class="faq faq-1 is-open">
    <h3 class="faq-header"><button>...</button></h3>
    <div class="faq-content">
        ...
    </div>
</div>

And this forces the following CSS rule to kick in and reveal the content of the clicked FAQ:

.faq.is-open .faq-content {
    visibility: visible;
    position: static;
}

Then, when the first FAQ's headline is clicked for the second time, and because we is-open is already present, it will be removed from the FAQ's parent container:

<div class="faq faq-1">
    <h3 class="faq-header"><button>...</button></h3>
    <div class="faq-content">
        ...
    </div>
</div>

So, this will force the following CSS rule to kick in again, hiding the content:

.faq-content {
    visibility: hidden;
    position: absolute;
    left: -9999px;
    padding: 0 30px 30px;
}

Basically, we are reverting back to the FAQ's initial closed state if it is currently open.

So, this results in:

0:00
/0:04

After dealing with "FAQ 1", we are doing the same with the remaining FAQ elements:

/* FAQ 2 */
const faq_2 = document.querySelector(".faq-2");
const faq_2_header = document.querySelector(".faq-2 .faq-header");

faq_2_header.addEventListener("click", function(){
    faq_2.classList.toggle("is-open");
});

/* FAQ 3 */
const faq_3 = document.querySelector(".faq-3");
const faq_3_header = document.querySelector(".faq-3 .faq-header");

faq_3_header.addEventListener("click", function(){
    faq_3.classList.toggle("is-open");
});

/* FAQ 4 */
const faq_4 = document.querySelector(".faq-4");
const faq_4_header = document.querySelector(".faq-4 .faq-header");

faq_4_header.addEventListener("click", function(){
    faq_4.classList.toggle("is-open");
});

So, now, all the FAQ headlines can reveal the content when someone clicks on them.

But as you already know, we are repeating a lot of code manually.

What if there are 20 FAQs or even 100 FAQs?

Such a large count of FAQs is a common thing in e-commerce stores. Here is an example.

So, we must automate the above code with the help of a "for" loop like this:

//Step 1: Select all FAQ headlines
const faqHeaders = document.querySelectorAll(".faqs .faq-header");

//Step 2: Loop through all selected FAQ headlines
for (let i = 0; i < faqHeaders.length; i++) {
  const faqHeader = faqHeaders[i];

  //Step 3: Add "click" event listener to each of them
  faqHeader.addEventListener("click", function () {

    /* 
    Step 4: When someone clicks on an individual FAQ headline, 
    toggle the "is-open" class on its parent element to make the
    FAQ content visible or hide it if it is already open
    */
    faqHeader.parentElement.classList.toggle("is-open");
  });
}

Come on, go ahead and type the above code inside the index.js file by removing the existing code (if there is any).

Is everything working as expected?

Yep!

Nice, anyway, for people who didn't understand the above code, let's go through it step-by-step in the next lesson.