React手写一个手风琴组件示例

目录
  • 知识点
  • 结构分析
  • AccordionItem子组件
  • Accordion容器组件

知识点

  • 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) =&gt; {
   //返回元素
};

子元素组件分成三个部分,一个容器元素,一个标题元素和一个内容元素,因此我们可以将结构写成如下:

<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) =&gt; {
  //后续代码
};

我们再来分析一下需要传给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&lt;AccordionType&gt;) =&gt; {
  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

源码地址 https://github.com/eveningwater/code-segment-react

以上就是React手写一个手风琴组件示例的详细内容,更多关于React手风琴组件的资料请关注我们其它相关文章!

(0)

相关推荐

  • 从零搭建react+ts组件库(封装antd)的详细过程

    目录 整体需求 开发与打包工具选型 使用webpack作为打包工具 使用babel来处理typescript代码 使用less-loader.css-loader等处理样式代码 项目搭建思路 整体结构 方案思路 项目搭建实施 初始化 Babel引入 了解Babel webpack的基于babel-loader的处理流程 引入React相关库(externals方式) 编写组件代码 编译打包组件库 效果演示 处理样式(less编译与css导出) 依赖引入 配置webpack 编写样式代码 编译组件

  • Vite + React从零开始搭建一个开源组件库

    目录 前言 目标 搭建开发环境

  • React函数组件与类组件使用及优劣对比

    目录 一.类组件的问题 原因一.因为this带来的问题: 问题描述 问题解析 原因二.类组件代码量比函数组件多: 原因三.类组件过于臃肿不易拆分: 二.函数组件的问题 挂载阶段:getDerviedStateFromProps VS 无 挂载阶段:UNSAFE_componentWillMount VS 无 挂载阶段:componentDidMount VS useEffect render: 生命周期,更新阶段:UNSAFE_componentWillRerciveProps VS 无 生命周

  • React父子组件传值(组件通信)的实现方法

    目录 1.父组件传值子组件 2.子组件传值父组件 3.兄弟组件传值 1.父组件传值子组件 在引用子组件的时候传递,相当于一个属性,例如:在子组件内通过porps.param获取到这个param的值. 父组件向子组件传值,通过props,将父组件的state传递给了子组件. 父组件代码片段: constructor(props){ super(props) this.state={ message:"i am from parent" } } render(){ return( <

  • ReactHooks+ts(函数组件)实现原生轮播的示例

    目录 1.下载依赖(第一个是js依赖,第二个是ts依赖) 2.创建tsx文件 3.创建less文件 1.下载依赖(第一个是js依赖,第二个是ts依赖) npm install smoothscroll-polyfill --save npm i --save-dev @types/smoothscroll-polyfill 2.创建tsx文件 import React, { useRef, useState, useEffect } from 'react' import './index.le

  • React手写一个手风琴组件示例

    目录 知识点 结构分析 AccordionItem子组件 Accordion容器组件 知识点 emotion语法 react语法 css语法 typescript类型语法 结构分析 根据上图,我们来分析一下,一个手风琴组件应该包含一个手风琴容器组件和多个手风琴子元素组件.因此,假设我们实现好了所有的逻辑,并写出使用demo,那么代码应该如下: <Accordion defaultIndex="1" onItemClick={console.log}> <Accordi

  • 在React中写一个Animation组件为组件进入和离开加上动画/过度效果

    在React中写一个Animation组件为组件进入和离开加上动画/过度效果 问题 在单页面应用中,我们经常需要给路由的切换或者元素的挂载和卸载加上过渡效果,为这么一个小功能引入第三方框架,实在有点小纠结.不如自己封装. 思路 原理 以进入时 opacity: 0 --> opacity: 1  ,退出时 opacity: 0 --> opacity: 1 为例 元素挂载时 1.挂载元素dom 2.设置动画 opacity: 0 --> opacity: 1 元素卸载时 1.设置动画 o

  • 尝试自己动手用react来写一个分页组件(小结)

    本文介绍了尝试自己动手用react来写一个分页组件(小结),分享给大家,具体如下: 分页效果 在线预览 github地址 效果截图(样式可自行修改): 构建项目 create-react-app react-paging-component 分页组件 1.子组件 创建 Pagecomponent.js 文件 核心代码: 初始化值 constructor(props) { super(props) this.state = { currentPage: 1, //当前页码 groupCount:

  • 使用React手写一个对话框或模态框的方法示例

    打算用React写对话框已经很长一段时间,现在是时候兑现承诺了.实际上,写起来相当简单. 核心在于使用React的接口React.createPortal(element, domContainer).该接口将element渲染后的DOM节点嵌入domContainer(通常是document.body),并保证只嵌入一次. 所以,我们可以这样写一个对话框或模态框: function Dialog() { return React.createPortal( <div>Dialog conte

  • react写一个select组件的实现代码

    之前一直用的antd的Select组件,但在有些端并不适用,而原生的select样式修改不灵活,遂产生自己写一个组件的想法.观察select组件: <select onChange={(value) => {this.value=value}} <option value='1'>man</option> <option value='0'>woman</option> </select> 可以看出数据都是在option中,有值val

  • 手写一个@Valid字段校验器的示例代码

    上次给大家讲述了 Springboot 中的 @Valid 注解 和 @Validated 注解的详细用法: 详解Spring中@Valid和@Validated注解用法 当我们用上面这两个注解的时候,需要首先在对应的字段上打上规则注解,类似如下. @Data public class Employee { /** 姓名 */ @NotBlank(message = "请输入名称") @Length(message = "名称不能超过个 {max} 字符", max

  • 利用Java手写一个简易的lombok的示例代码

    目录 1.概述 2.lombok使用方法 3.lombok原理解析 4.手写简易lombok 1.概述 在面向对象编程中,必不可少的需要在代码中定义对象模型,而在基于Java的业务平台开发实践中尤其如此.相信大家在平时开发中也深有感触,本来是没有多少代码开发量的,但是因为定义的业务模型对象比较多,而需要重复写Getter/Setter.构造器方法.字符串输出的ToString方法.Equals/HashCode方法等.我们都知道Lombok能够替大家完成这些繁琐的操作,但是其背后的原理很少有人会

  • Java实现手写一个线程池的示例代码

    目录 概述 线程池框架设计 代码实现 阻塞队列的实现 线程池消费端实现 获取任务超时设计 拒绝策略设计 概述 线程池技术想必大家都不陌生把,相信在平时的工作中没有少用,而且这也是面试频率非常高的一个知识点,那么大家知道它的实现原理和细节吗?如果直接去看jdk源码的话,可能有一定的难度,那么我们可以先通过手写一个简单的线程池框架,去掌握线程池的基本原理后,再去看jdk的线程池源码就会相对容易,而且不容易忘记. 线程池框架设计 我们都知道,线程资源的创建和销毁并不是没有代价的,甚至开销是非常高的.同

  • vue用Object.defineProperty手写一个简单的双向绑定的示例

    前言 上次写了一个Object.defineProperty() 不详解,文末说要写用它来写个双向绑定.说话算话,说来就来 前文链接 Object.defineProperty() 不详解 先看最后效果 model演示.gif 什么是双向绑定? 1.当一个对象(或变量)的属性改变,那么调用这个属性的地方显示也应该改变,模型到视图(model => view) 2.当调用属性的这个地方改变了这个属性(通常是一个表单元素),那么这个对象(或变量)的属性也会改为最新的值 ,即视图到模型(view =>

  • Python实现手写一个类似django的web框架示例

    本文实例讲述了Python实现手写一个类似django的web框架.分享给大家供大家参考,具体如下: 用与django相似结构写一个web框架. 启动文件代码: from wsgiref.simple_server import make_server #导入模块 from views import * import urls def routers(): #这个函数是个元组 URLpattern=urls.URLpattern return URLpattern #这个函数执行后返回这个元组

随机推荐