0
(0)

Let’s get with the task crystal clear and write some points of what types of accordion will we create and what should the requirements be:

Let’s do the only-one-opened-at-a-time accordion !

I will structure the code in 2 components:

  1. The Faq.tsx file that will contain the code fetching the items data from the json file and will send one by one to the child component FaqItem the question and the answer data and the index of that item in the stack of accordion items. Also we will send to each child item the state that says if that item is opened or not and also a method that will give power to the child element to send information back to the parent component that it got a click and that means that from now on that child element is opened.
  2. the FaqItem.tsx file will contain the actual accordion item generated from the json data(Question and Answer fields)

I want to have the entire code before I decide to read the whole article, ok?

Yes, of course, let me provide that to you:

Faq.tsx

import React, { useEffect, useState } from 'react';
import FaqItem from './FaqItem';
import styled from 'styled-components';

export interface FaqDataTypes {
    question: string,
    answer: string
}

const DefList = styled.dl`
    font-size: 20px;
    counter-reset: deflist;
    dt {
       display: flex;
       background: #ddd;
       margin-bottom: 2px;
       padding: 10px;
       counter-increment: deflist;
       position: relative;
       cursor: pointer;
       &::before {
        content: 'Q'counter(deflist)':';
        padding-right: 5px;
       } 
       &::after {
        content: '?';
       }
       span {
        background: transparent;
        border: 0;
        height: 100%;
        font-size: 30px;
        position: absolute;
        top:0;
        right:0;
        cursor: pointer;
        width: 30px;
       }
    }
    dd {
        opacity: 0;
        background: #eee;
        margin: 0 0 2px;
        padding: 0 10px 0 45px;
        max-height: 0;
        overflow: hidden;
        transition: max-height .6s ease-in-out, opacity .8s;
        box-sizing: border-box;
        &::before,
        &::after {
            content: '';
            display: block;
            height: 10px;
            width 100%;
        }
        &.expanded {
            max-height: 105px;
            opacity: 1
        }    
    }
`;

const Faq = () => {
    const [faqData, setFaqData] = useState([]);
    const [isExpanded, setIsExpanded] = useState(0);

    useEffect(() => {
        fetch('http://localhost:4001/faq')
            .then(response => response.json())
            .then(setFaqData);
    },[]);

    return (
        <DefList>
            {
                faqData.map((data, index) => (
                    <FaqItem 
                        key={index}
                        faqItemData={data} 
                        index={index} 
                        isExpanded={isExpanded === index} 
                        setIsExpanded={setIsExpanded} />
                ))
            }
        </DefList>
    )
}

export default Faq;

FaqItem.tsx

import React, { useState } from 'react';
import { FaqDataTypes } from './Faq';

interface FaqItemProps {
    faqItemData: FaqDataTypes;
    index: number;
    isExpanded: boolean;
    setIsExpanded: React.Dispatch<React.SetStateAction<number>>;
}

const FaqItem = ({faqItemData, index, isExpanded, setIsExpanded}:FaqItemProps) => {
    const {question, answer} = faqItemData;

    const handleClick = (index: number) => {
        setIsExpanded(index);
    }

    return (
        <>
            <dt onClick={() => handleClick(index)}>
            {/* <dt onClick={() => setIsExpanded(!isExpanded)}> */}
                {question}
                <span>{isExpanded? '-' : '+'}</span>
            </dt>
            <dd className={isExpanded ? 'expanded' : ''}>{answer}</dd>
        </>
    )
}

export default FaqItem;

Let me explain !

First things first, we start with the parent component, the Faq.tsx. We need to import the useEffect and useState hooks. I will explain soon what are they used for. We import also the styled components to write some CSS and we also import the child component to reference it.

We need to start fetching the data from the json file as a preliminary step

For the json file I used the mockoon service, where I’ve set up a url and the mock data for our task. The actual fetching will happen on the component load, only one time, so for this I used the useEffect hook:

The mockoon service actually created a local server where it serves our Json mock data. the last .then() event listener (it is the oncomplete event listener) can be shortened like this and it will mean the same thing:

What we did here is, we set the value of the faqData state to the data we got from the json. This way when data fetch is complete, the component with all child components will be rerendered with the content. The type of the data is stored in an interface called FaqDataTypes and it is an array of that type model( FaqDataTypes[] ). Also we give an initial value to that state of an empty array to avoid errors when trying to map that state. The typescript interface and the json data looks like this:

Now we iterate through the data from the faqData react state. Of course we give a key attribute to each child element(FaqItem), we also send to the child element the data from the Json(faqItemData={data}. The index attribute and the isExpanded, setExpanded state values are also important for the child component. This way we can have a communication between child and parent component and have an “agreement” which of the items is expanded and which is collapsed.

Show me the child element, what does it do?

First of all it returns a <dt> and <dd> elements, that is pretty semantic for an accordion I think. The question and answer variables hold the actual text data from the json.(we descructured it at the beginning of the component).

As you can see we used here another interface called FaqItemProps that holds all type definitions for the props that this component gets.

Since we exported the FaqDataTypes, we can easily use it here for the faqItemData value after we imported it also here. The setIsExpanded function has a value that is easily taken when hovering ofer the setIsExpanded prop, let me show you:

The isExpanded prop holds a boolean value, that if it is true it will change the symbol to minus sign and if it is false then the + will reappear.

Also we used the isExpanded on the class name of the answer field, when it’s true the class name will be “expanded”, on false there will be no class name. I used classes because I want the slideDown and slideUp to be transitioned only with CSS.

When clicking on the question’s field it will access the handleClick function and it will send the current index of that element:

Now let’s get back to the parent element where the setIsExpanded(index) arrives with the index of that child element, that was clicked:

After the isExpanded state is updated, the component rerenders so the isExpanded prop is evaluated again, checking if the last clicked items index matches the index from the mapping of the array of elements. If matches the value will be true and the child component will render the accordion item expanded.

How about the styled-component’s CSS ?

For the numbering of the questions we will use a CSS counter.

We named that counter “deflist” and we set it up on the <dl> element, we increment it on each <dt> element and we use it on each ::before pseudo-element of the <dt> element between the ‘Q” and the “:” characters.

Instead of adding in the data a question mark for each question, we add it dynamically with the ::after pseudo-element:

How about the slideUp and slideDown effect?

Basically we play around with the max-height: 0 and max-height: 105px but that value depends of your content. If you think your content will be bigger then just put a much bigger value. We transition the opacity and the max-height values from 0 to a value.

The problem here is that vertical paddings are not nicely transitioned in CSS so I did a little trick here. I added a ::before and ::after pseudo-element on each question that has the height of the top and bottom space, that otherwise would have been a padding-top and padding-bottom. This way there is no glitch when transitioning those elements.

That’s about it.

I want the version where I can open multiple Faq answers !

Faq.tsx

import React, { useEffect, useState } from 'react';
import FaqItem from './FaqItem';
import styled from 'styled-components';

export interface FaqDataTypes {
    question: string,
    answer: string
}

const DefList = styled.dl`
    font-size: 20px;
    counter-reset: deflist;
    dt {
       display: flex;
       background: #ddd;
       margin-bottom: 2px;
       padding: 10px;
       counter-increment: deflist;
       position: relative;
       cursor: pointer;
       &::before {
        content: 'Q'counter(deflist)':';
        padding-right: 5px;
       } 
       &::after {
        content: '?';
       }
       span {
        background: transparent;
        border: 0;
        height: 100%;
        font-size: 30px;
        position: absolute;
        top:0;
        right:0;
        cursor: pointer;
        width: 30px;
       }
    }
    dd {
        opacity: 0;
        background: #eee;
        margin: 0 0 2px;
        padding: 0 10px 0 45px;
        max-height: 0;
        overflow: hidden;
        transition: max-height .6s ease-in-out, opacity .8s;
        box-sizing: border-box;
        &::before,
        &::after {
            content: '';
            display: block;
            height: 10px;
            width 100%;
        }
        &.expanded {
            max-height: 105px;
            opacity: 1
        }    
    }
`;

const Faq = () => {
    const [faqData, setFaqData] = useState<FaqDataTypes[]>([]);


    useEffect(() => {
        fetch('http://localhost:4001/faq')
            .then(response => response.json())
            .then(setFaqData);
    },[]);

    return (
        <DefList>
            {
                faqData.map((data, index) => (
                    
                    <FaqItem 
                        key={index}
                        faqItemData={data}  />
                ))
            }
        </DefList>
    )
}

export default Faq;

FaqItem.tsx

import React, { useState, Key } from 'react';
import { FaqDataTypes } from './Faq';

interface FaqProps {
    key: Key;
    faqItemData: FaqDataTypes;
}

const FaqItem = ({faqItemData}:FaqProps) => {
    const {question, answer} = faqItemData;
    const [isExpanded, setIsExpanded] = useState(false);

    return (
        <>
            
            <dt onClick={() => setIsExpanded(!isExpanded)}>
                {question}
                <span>{isExpanded? '-' : '+'}</span>
            </dt>
            <dd className={isExpanded ? 'expanded' : ''}>{answer}</dd>
        </>
    )
}

export default FaqItem;

In this scenario we are not sending any state to the child component, because each of them is responsible only to open itself and close itself. No back-and-forth messaging is needed to tell the parent that “I am expanded, tell the other siblings to close themselves.

Another interesting fact is, you can use the React.Key type to define also the key prop.

Alright, if you have any questions, just add a comment. If not, just add a like and hit the subscription button.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.