封装react组件库之Collapse

Collapse组件折叠板

在后台系统中这个应该还是很常见用的可能不是很频繁,但是有时候还是很有适用的场景的,现在我们就来从零封装一个完整的Collapse组件。
这个是我封装好了的一个成品。我们封装组件第一步就要先去思考,

第一这个组件需要什么样的功能。
第二这个组件的一个功能确定了这个组件的代码结构,也就是常说的dom结构。
第三开始思考两小时,然后我们开始代码编写。

在之前的组件封装中我也提到过,有兴趣可以去看看我的博客其余文章,很多的组件封装我都会分享出来。
在这里插入图片描述
看到这样的结构我们这样的老油条前端大致心里面就有一个结构了:

//content
<div>
//contentItem
	<div>
	//contentItem header
		<div>
			<span></span>
			<span></span>
		</div>
		//contentItem content
		<div></div>
	</div>
	
<div>

这就是一个大致的dom结构了,我们在使用市面上的Collapse组件的时候很多时候都是这样

<Collapse>
	<Panel></Panel>
</Collapse>

在组件中我们都说一个组件化组件化,意思就是能尽可能抽成组件复用的我们都会抽成一个组件。在这里我们就可以把content抽成一个Collapse组件然后把contentItem抽成一个Panel组件。
这个就是Collapse的组件代码,很简单没有特别的复杂代码,props.children传递参数在我之前的博客我也有一篇拿来专门讲过,有兴趣的可以去看看。

/*
 * @Date: 2023-08-21 13:40:50
 * @Auth: 463997479@qq.com
 * @LastEditors: 463997479@qq.com
 * @LastEditTime: 2023-08-22 16:21:33
 * @FilePath: \reactui\src\Collapse\index.tsx
 */
import classNames from 'classnames';
import React, { forwardRef, useState } from 'react';
import Panel from './Panel';

type CollapseType = {
  children?: any;
  defaultKey?: string | string[] | number | number[];
  accordion?: boolean;
  onChange?: (arg: string | number | string[] | number[]) => void;
  className?: string;
  expandIconPosition?: string;
  collapseable?: string;
};

const Collapse: React.FC<CollapseType> = forwardRef((props, ref) => {
  const {
    children,
    defaultKey = [],
    accordion = false,
    onChange,
    className,
    expandIconPosition = 'start',
    collapseable = false,
  } = props;
  const contentClass = classNames('collapse-content', className, {
    [`collapse-content-${expandIconPosition}`]: expandIconPosition,
  });
  const [activeKey, setActiveKey] = useState<
    string | number | string[] | number[]
  >(defaultKey ?? '');

  const handleActive = (arg: string | number) => {
    if (Array.isArray(activeKey)) {
      const _value = activeKey.indexOf(arg) > -1 ? '' : arg;
      if (accordion) {
        setActiveKey(_value);
        onChange?.(_value);
        return;
      }
      let arr = [];
      let flag = activeKey.some((item) => item === arg);
      if (flag) {
        arr = activeKey.filter((item) => item !== arg) as string[] | number[];
      } else {
        arr = activeKey.concat([arg]) as string[] | number[];
      }
      setActiveKey(arr);
      onChange?.(arr);
    } else {
      setActiveKey(arg === activeKey ? '' : arg);
      onChange?.(arg);
    }
  };
  return (
    <>
      <div ref={ref} className={contentClass}>
        {React.Children.map(children, (child) => {
          return React.cloneElement(child, {
            active: activeKey,
            handleActive: handleActive,
          });
        })}
      </div>
    </>
  );
});

Collapse.Panel = Panel;
Collapse.displayName = 'Collapse';
Collapse.defaultProps = {
  accordion: false,
  defaultKey: [],
};
export default Collapse;

然后就是Panel组建的封装了,这个子组件也没有太大的难度,前面我们的方案已经很清楚了所以写起来也没有太大的难度,唯一一个问题很有难度我觉得有必要拿出来单独讲一下。

css怎么给一个高度auto的dom过度高度动画?你们有没有想过这个问题去如何实现呢?如果这个问题你能有很好的解决办法那这个组件也没有太大的难度,这个组件的难度就在这个动画。

第一种:一个外部容器很大的max-height值overflow,内部容器heigh:auto;然后用过渡外部容器的动画来显示。
第二种:容器直接设置transform:scale(0);transition-origin:center top;过渡到transform:scale(1);
第三种:容器display:grid;grid-template-rows:0fr;过渡到grid-template-rows:1fr;
这些方法或多或少都有一些不足的地方。最完美的还得js。
Flip动画思想应该有了解吧。先让达到一个预期的目标比如高度然后我们就能拿到这个值,拿到了这个值我们就可以对这个制作一些动画。

这都是实现的一个方向,这个组件我也是用了Flip动画。
具体的选中这些看一下代码大致就能明白这里就不做详细的说明了。

/*
 * @Date: 2023-08-21 13:40:50
 * @Auth: 463997479@qq.com
 * @LastEditors: 463997479@qq.com
 * @LastEditTime: 2023-08-22 17:04:21
 * @FilePath: \reactui\src\Collapse\Panel.tsx
 */
import { RightOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
type PanelType = {
  children?: any;
  title?: string;
  activeKey: string | number;
  active?: string | string[];
  handleActive?: (arg: string | number) => void;
  extra?: React.ReactNode;
};

const Panel: React.FC<PanelType> = (props) => {
  const { children, title, activeKey, active, handleActive, extra } = props;
  const [onlyKey, setOnlyKey] = useState<string | number>(activeKey);
  const [show, setShow] = useState<boolean>(false);
  const [domShow, setDomShow] = useState<boolean>(false);

  useEffect(() => {
    let flag =
      active === onlyKey ||
      ((Array.isArray(active) && active?.indexOf(onlyKey) > -1) as boolean);
    setOnlyKey(activeKey);
    setShow(flag);
  }, [activeKey, active, domShow]);
  const contentRef = useRef();
  const contentClass = classNames('panel-content-item', {
    ['panel-content-item-active']: domShow,
  });
  const wrapperClass = classNames('panel-content-item-wrapper', {
    ['panel-content-item-wrapper-active']: domShow,
  });
  const onEnter = (el: HTMLElement) => {
    el.style.height = '0px';
    el.style.overflow = 'hidden';
    setDomShow(true);
  };

  const onEntering = (el: HTMLElement) => {
    el.style.height = el.scrollHeight + 'px';
  };

  const onEntered = (el: HTMLElement) => {
    el.style.transition = '';
    el.style.height = '';
  };

  const onExit = (el: HTMLElement) => {
    el.style.overflow = 'hidden';
    el.style.height = el.scrollHeight + 'px';
  };

  const onExiting = (el: HTMLElement) => {
    if (el.scrollHeight !== 0) {
      el.style.height = '0';
    }
  };
  // 为什么要加el.scrollHeight !== 0的判断呢?
  //试一下,如果不加这个判断,直接变化height,paddingTop,paddingBottom的值到0,这个时候,收缩时并不会有过渡动画,元素马上就消失了。

  //setTimeout(() => {el.style.height = 0;el.style.paddingTop = 0;el.style.paddingBottom = 0;}, 20)
  const onExited = (el: HTMLElement) => {
    el.style.transition = '';
    el.style.height = '';
    setDomShow(false);
  };
  return (
    <>
      <div className={contentClass}>
        <div
          onClick={() => {
            handleActive?.(onlyKey);
          }}
          className="collapse-header"
        >
          <span className="collapse-header-img">
            <RightOutlined />
          </span>
          <span className="collapse-header-title">{title}</span>
          <span>{extra}</span>
        </div>
        <CSSTransition
          in={show}
          onEnter={onEnter}
          onEntering={onEntering}
          onEntered={onEntered}
          onExit={onExit}
          onExiting={onExiting}
          onExited={onExited}
          classNames={'collapse'}
          timeout={200}
        >
          <div ref={contentRef}>
            <div className={wrapperClass}>{children}</div>
          </div>
        </CSSTransition>
      </div>
    </>
  );
};
export default Panel;

demo示例
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值