乐趣区

关于react.js:使用React手写一个手风琴组件

知识点

  • emotion 语法
  • react 语法
  • css 语法
  • typescript 类型语法

成果

让咱们来看一下咱们实现的效果图:

构造剖析

依据上图,咱们来剖析一下,一个手风琴组件应该蕴含一个手风琴容器组件和多个手风琴子元素组件。因而,假如咱们实现好了所有的逻辑,并写出应用 demo,那么代码应该如下:

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

依据以上的构造,咱们能够得悉,首先容器组件 Accordion 会裸露一个 defaultIndex 属性以及一个 onItemClick 事件。顾名思义,defaultIndex 代表默认开展的子元素组件 AccordionItem 的索引,onItemClick 代表点击每一个子元素组件所触发的事件。而后,咱们能够看到子元素组件有 label 属性和 index 属性,很显然,label 代表以后子元素的题目,index 代表以后子元素组件的索引值,而咱们的 Lorem ipsum 就是子元素的内容。依据这些剖析,咱们先来实现一下 AccordionItem 组件。

AccordionItem 子组件

首先咱们定义好子组件的构造,函数组件写法如下:

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

子元素组件分成三个局部,一个容器元素,一个题目元素和一个内容元素,因而咱们能够将构造写成如下:

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

晓得了构造之后,咱们就晓得 props 会有哪些属性,首先是索引 index 属性,它的类型为 string 或者 number,而后是判断内容是否开展的属性 isCollapsed, 它的类型是布尔值,其次咱们还有渲染题目的属性 label,它应该是一个 react 节点,类型为 ReactNode,同理,还有一个内容属性即 children, 类型也应该是 ReactNode, 最初就是咱们要裸露的事件办法 handleClick, 它的类型应该是一个办法,因而咱们能够定义如下的接口:

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

接口定义好之后,接下来咱们就在接口外面拿值(采纳对象解构的形式), 这些值都算是可选的,即:

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

此时咱们的 AccordionItem 子组件应该是如下:

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

这里咱们能够应用 emotion/css 来写 css 类名款式,代码如下:

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

以上的 css 前面跟模板字符串再跟 css 款式就是 emotion/css 语法,cx也就是组合款式写法,款式都是惯例的写法,也没什么好说的。这里有一个难点,那就是 display:none 和 display:block 没有过渡成果,因而能够采纳 visibility:hidden 和 opacity:0 的形式来替换,然而这里为了简略,没思考动画成果,所以也就将问题放着,前面有工夫再优化。

到目前为止,这个子组件就算是实现了,这也就意味着咱们的手风琴组件曾经实现一半了,接下来咱们来看容器组件 Accordion 的写法。

Accordion 容器组件

首先咱们先把构造写好:

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

咱们再来剖析一下须要传给 Accordion 组件的属性有哪些,很显然有 defaultIndex,onItemClick 和 children,因而咱们能够定义如下的接口:

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

留神这里的 children 不应该是 ReactNode,而是 JSX.Element 元素数组,这是为什么呢,咱们前面再来解释这个问题。当初咱们晓得了 props 的属性之后,咱们能够拿到这些属性,代码如下:

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

当初咱们再保护一个状态,用来代表以后显示的子元素组件的索引,应用 useState hook 函数,初始化默认值就应该是 defaultIndex。如下:

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

接下来,咱们编写好容器元素,并写好款式,如下所示:

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

容器元素的款式如下:

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;
  `,
);

好的,接下来,咱们实际上容器元素的子元素应该是多个 AccordionItem 元素,也正因为如此,这里的 children 类型就是JSX.Element [],咱们应该如何获取这些子元素呢?咱们应该晓得,每一个子元素对应的就是一个节点,在 react 中用的是链表来示意这些节点,每个节点对应的就有个 type 属性,咱们只须要拿到容器元素的子组件元素中 type 属性为 AccordionItem 的元素数组,如下:

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

到了这里,咱们就晓得了,容器元素的子元素是一个数组,咱们就须要遍历,应用 map 办法,如下:

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

请留神这一段代码:

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

这就是咱们之前子组件绑定的事件,也是咱们须要裸露进来的事件,在这个事件办法中,咱们无非执行的就是更改以后被开展元素的索引。所以代码就很好写了:

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

到了这里,咱们的一个手风琴组件就实现了,残缺代码如下:

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

让咱们来看一下成果:

到此为止了,更多 React 组件的实现,能够拜访 react-code-segment。

源码地址能够看这里源码地址。喜爱感觉不错可能帮忙到您,心愿能点个赞,您的赞就是我更新文章的最大能源。

退出移动版