头图

Knowledge point

  • emotion grammar
  • react syntax
  • css syntax
  • typescript type syntax

Effect

Let's take a look at the renderings of our implementation:

Structural Analysis

According to the above figure, let's analyze that an accordion component should contain an accordion container component and multiple accordion sub-element components. Therefore, assuming that we have implemented all the logic and written to use the demo, the code should be as follows:

 <Accordion defaultIndex="1" onItemClick={console.log}>
   <AccordionItem label="A" index="1">
     Lorem ipsum
   </AccordionItem>
   <AccordionItem label="B" index="2">
      Dolor sit amet
   </AccordionItem>
</Accordion>

According to the above structure, we can know that first the container component Accordion will expose a defaultIndex property and an onItemClick event. As the name implies, defaultIndex represents the index of the default expanded child element component AccordionItem , and onItemClick represents the event triggered by clicking each child element component. Then, we can see that the child element component has a label attribute and an index attribute. Obviously, the label represents the title of the current child element, the index represents the index value of the current child element component, and our Lorem ipsum is the content of the child element. Based on these analyses, let's first implement the AccordionItem component.

AccordionItem child component

First, we define the structure of the subcomponent. The function component is written as follows:

 const AccordionItem = (props) => {
   //返回元素
};

The child element component is divided into three parts, a container element, a title element and a content element, so we can write the structure as follows:

 <div className="according-item-container">
   <div className="according-item-header"></div>
   <div className="according-item-content"></div>
</div>

After knowing the structure, we know what properties props will have. First, the index attribute, which is of type string or number, and then the attribute isCollapsed, which determines whether the content is expanded, whose type is a boolean value, and secondly, we also have rendering The attribute label of the title, it should be a react node, the type is ReactNode, for the same reason, there is also a content attribute that is children, the type should also be ReactNode, and finally we want to expose the event method handleClick, its type should be a method , so we can define the following interface:

 interface AccordionItemType {
  index: string | number;
  label: string;
  isCollapsed: boolean;
  //SyntheticEvent代表react合成事件对象的类型
  handleClick(e: SyntheticEvent): void;
  children: ReactNode;
}

After the interface is defined, then we take values in the interface (using object destructuring), these values are optional, namely:

 const { label, isCollapsed, handleClick, children } = props;

At this point our AccordionItem subcomponent should look like this:

 const AccordionItem = (props: Partial<AccordionItemType>) => {
  const { label, isCollapsed, handleClick, children } = props;
  return (
    <div className={AccordionItemContainer} onClick={handleClick}>
      <div className={AccordionItemHeader}>{label}</div>
      <div
        aria-expanded={isCollapsed}
        className={`${AccordionItemContent}${
          isCollapsed ? ' collapsed' : ' expanded'
        }`}
      >
        {children}
      </div>
    </div>
  );
};

Here we can use emotion/css to write the css class name style, the code is as follows:

 const baseStyle = css`
  line-height: 1.5715;
`;
const AccordionItemContainer = css`
  border-bottom: 1px solid #d9d9d9;
`;
const AccordionItemHeader = cx(
  baseStyle,
  css`
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-start;
    padding: 12px 16px;
    color: rgba(0, 0, 0, 0.85);
    cursor: pointer;
    transition: all 0.3s, visibility 0s;
    box-sizing: border-box;
  `,
);


const AccordionItemContent = css`
  color: #000000d9;
  background-color: #fff;
  border-top: 1px solid #d9d9d9;
  transition: all 0.3s ease-in-out;
  padding: 16px;
  &.collapsed {
    display: none;
  }
  &.expanded {
    display: block;
  }
`;

The above css followed by the template string followed by the css style is the emotion/css syntax, cx that is, the combined style writing method, the styles are all conventional writing methods, and there is nothing to say. There is a difficulty here, that is, display:none and display:block have no transition effect, so they can be replaced by visibility:hidden and opacity:0, but for simplicity, the animation effect is not considered, so the problem is left. , there will be time to optimize later.

So far, this sub-component is complete, which means that our accordion component is half completed. Next, let's look at the container component Accordion Writing.

Accordion container component

First, let's write the structure:

 const Accordion = (props) => {
  //后续代码
};

Let's analyze the properties that need to be passed to the Accordion component. Obviously, there are defaultIndex, onItemClick and children, so we can define the following interface:

 interface AccordionType {
  defaultIndex: number | string;
  onItemClick(key: number | string): void;
  children: JSX.Element[];
}

Note that the children here should not be ReactNode, but an array of JSX.Element elements. Why is this, we will explain this problem later. Now that we know the properties of props, we can get these properties, the code is as follows:

 const Accordion = (props:Partial<AccordionType>) => {
  const { defaultIndex, onItemClick, children } = props;
  //后续代码
};

Now we maintain a state to represent the index of the currently displayed sub-element component. Using the useState hook function, the initialized default value should be defaultIndex. as follows:

 const Accordion = (props:Partial<AccordionType>) => {
  const { defaultIndex, onItemClick, children } = props;
  //新增的代码
  const [bindIndex, setBindIndex] = useState(defaultIndex);
  //后续代码
};

Next, we write the container element, and write the style, as follows:

 const Accordion = (props: Partial<AccordionType>) => {
  const { defaultIndex, onItemClick, children } = props;
  const [bindIndex, setBindIndex] = useState(defaultIndex);
  return (
    <div className={AccordionContainer}></div>
  );
};

The style of the container element is as follows:

 const baseStyle = css`
  line-height: 1.5715;
`;
const AccordionContainer = cx(
  baseStyle,
  css`
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    color: #000000d9;
    font-size: 14px;
    background-color: #fafafa;
    border: 1px solid #d9d9d9;
    border-bottom: 0;
    border-radius: 2px;
  `,
);

Ok, next, we actually should have multiple AccordionItem elements as the child elements of the container element, and because of this, the children type here is JSX.Element [] , how should we get these child elements? We should know that each child element corresponds to a node. In react, a linked list is used to represent these nodes. Each node corresponds to a type attribute. We only need to get the type in the child component element of the container element. The attribute is an array of elements of AccordionItem, as follows:

 //name不是AccordionItem,代表子元素不是AccordionItem,不是的我们需要过滤掉
const items = children?.filter(
    (item) => item?.type?.name === 'AccordionItem,代表子元素不是AccordionItem,所以我们需要过滤掉',
 );

At this point, we know that the child element of the container element is an array, we need to traverse, using the map method, as follows:

 items?.map(({ props: { index, label, children } }) => (
  <AccordionItem
     key={index}
     label={label}
     children={children}
     isCollapsed={bindIndex !== index}
     handleClick={() => changeItem(index)}
  />
))

Note this piece of code:

 handleClick={() => changeItem(index)}

This is the event we bound to the child component before, and it is also the event we need to expose. In this event method, all we do is change the index of the currently expanded element. So the code is easy to write:

 const changeItem = (index: number | string) => {
   //暴露点击事件方法接口
   if (typeof onItemClick === 'function') {
     onItemClick(index);
   }
   //设置索引
   if (index !== bindIndex) {
     setBindIndex(index);
   }
};

At this point, one of our accordion components is completed, and the complete code is as follows:

 import { cx, css } from '@emotion/css';
import React, { useState } from 'react';
import type { ReactNode, SyntheticEvent } from 'react';


const baseStyle = css`
  line-height: 1.5715;
`;
const AccordionContainer = cx(
  baseStyle,
  css`
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    color: #000000d9;
    font-size: 14px;
    background-color: #fafafa;
    border: 1px solid #d9d9d9;
    border-bottom: 0;
    border-radius: 2px;
  `,
);
const AccordionItemContainer = css`
  border-bottom: 1px solid #d9d9d9;
`;
const AccordionItemHeader = cx(
  baseStyle,
  css`
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-start;
    padding: 12px 16px;
    color: rgba(0, 0, 0, 0.85);
    cursor: pointer;
    transition: all 0.3s, visibility 0s;
    box-sizing: border-box;
  `,
);


const AccordionItemContent = css`
  color: #000000d9;
  background-color: #fff;
  border-top: 1px solid #d9d9d9;
  transition: all 0.3s ease-in-out;
  padding: 16px;
  &.collapsed {
    display: none;
  }
  &.expanded {
    display: block;
  }
`;


interface AccordionItemType {
  index: string | number;
  label: string;
  isCollapsed: boolean;
  handleClick(e: SyntheticEvent): void;
  children: ReactNode;
}
interface AccordionType {
  defaultIndex: number | string;
  onItemClick(key: number | string): void;
  children: JSX.Element[];
}


const AccordionItem = (props: Partial<AccordionItemType>) => {
  const { label, isCollapsed, handleClick, children } = props;
  return (
    <div className={AccordionItemContainer} onClick={handleClick}>
      <div className={AccordionItemHeader}>{label}</div>
      <div
        aria-expanded={isCollapsed}
        className={`${AccordionItemContent}${
          isCollapsed ? ' collapsed' : ' expanded'
        }`}
      >
        {children}
      </div>
    </div>
  );
};


const Accordion = (props: Partial<AccordionType>) => {
  const { defaultIndex, onItemClick, children } = props;
  const [bindIndex, setBindIndex] = useState(defaultIndex);
  const changeItem = (index: number | string) => {
    if (typeof onItemClick === 'function') {
      onItemClick(index);
    }
    if (index !== bindIndex) {
      setBindIndex(index);
    }
  };
  const items = children?.filter(
    (item) => item?.type?.name === 'AccordionItem',
  );
  return (
    <div className={AccordionContainer}>
      {items?.map(({ props: { index, label, children } }) => (
        <AccordionItem
          key={index}
          label={label}
          children={children}
          isCollapsed={bindIndex !== index}
          handleClick={() => changeItem(index)}
        />
      ))}
    </div>
  );
};

Let's take a look at the effect:

That's it for more implementations of React components, you can visit react-code-segment .

The source code address can be found here. If you like it, it can help you. I hope you can give it a like. Your like is the biggest motivation for me to update the article.


夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。