乐趣区

关于前端:每日一个组件如何从0到1实现一个折叠组件

点击在线浏览,体验更好 链接
古代 JavaScript 高级小册 链接
深入浅出 Dart 链接
古代 TypeScript 高级小册 链接

jcode

咱们先从最根本的实现开始,而后逐渐增加更多的性能,如手风琴模式、自定义箭头、禁用状态、暗藏时是否渲染 DOM 构造

组件接口定义

Collapse

属性 阐明 类型 默认值
accordion 是否开启手风琴模式 boolean false
activeKey 以后开展面板的 key 手风琴模式:string \ null 非手风琴模式:string[]
arrow 自定义箭头,如果是 ReactNode,那么 antd-mobile 会主动为你减少旋转动画成果 ReactNode \ ((active: boolean) => React.ReactNode)
defaultActiveKey 默认开展面板的 key 手风琴模式:string \ null 非手风琴模式:string[]
onChange 切换面板时触发 手风琴模式:(activeKey: string \ null) => void 非手风琴模式:(activeKey: string[]) => void

Collapse.Panel

属性 阐明 类型 默认值
arrow 自定义箭头 ReactNode \ ((active: boolean) => React.ReactNode)
destroyOnClose 不可见时卸载内容 boolean false
disabled 是否为禁用状态 boolean false
forceRender 被暗藏时是否渲染 DOM 构造 boolean false
key 惟一标识符 string
onClick 标题栏的点击事件 (event: React.MouseEvent<Element, MouseEvent>) => void
title 标题栏左侧内容 ReactNode

创立根底 Collapse 组件

咱们创立一个根底的 Collapse 组件。这个组件须要有一个状态来追踪它是否被开展

import React, {useState} from 'react';

const Collapse = ({children}) => {const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Collapse' : 'Expand'}
      </button>
      {isOpen && <div>{children}</div>}
    </div>
  );
};

export default Collapse;

拓展 Collapse 组件其它属性

  • accordion:如果设置为 true,咱们将启用手风琴模式。在这种模式下,只有一个面板能够被开展。当一个新的面板被开展时,之前开展的面板将被敞开。
  • activeKey:这是以后开展面板的 key。如果咱们处于手风琴模式,这将是一个字符串或 null。如果咱们不在手风琴模式,这将是一个字符串数组。
  • arrow:这是一个自定义的箭头。如果这是一个 React 节点,antd-mobile 将主动为你增加旋转动画成果。如果这是一个函数,它将接管一个参数,示意面板是否被开展,并返回一个 React 节点。
  • defaultActiveKey:这是默认开展面板的 key。它的类型与 activeKey 雷同。
  • onChange:这是一个函数,它在面板切换时被触发。它接管一个参数,示意以后开展面板的 key。它的类型与 activeKey 雷同。
import React, {useState, useEffect} from 'react';

const Collapse = ({children, forceRender, accordion, activeKey, arrow, defaultActiveKey, onChange}) => {const [isOpen, setIsOpen] = useState(false);
  const [currentActiveKey, setCurrentActiveKey] = useState(defaultActiveKey);

  useEffect(() => {setCurrentActiveKey(activeKey);
  }, [activeKey]);

  const handleClick = () => {setIsOpen(!isOpen);
    if (accordion) {setCurrentActiveKey(isOpen ? null : activeKey);
    }
    onChange && onChange(isOpen ? null : activeKey);
  };

  const renderArrow = () => {if (typeof arrow === 'function') {return arrow(isOpen);
    }
    return arrow;
  };

  return (
    <div>
      <button onClick={handleClick}>
        {isOpen ? 'Collapse' : 'Expand'}
        {renderArrow()}
      </button>
      <div style={{display: isOpen || forceRender ? 'block' : 'none'}}>
        {children}
      </div>
    </div>
  );
};

export default Collapse;

实现 Panel

咱们创立一个名为 Collapse.Panel 的子组件来反对这些新的属性。这个子组件将作为 Collapse 组件的一部分,用于示意一个可折叠的面板。

  • arrow:这是一个自定义的箭头。如果这是一个 React 节点,antd-mobile 将主动为你增加旋转动画成果。如果这是一个函数,它将接管一个参数,示意面板是否被开展,并返回一个 React 节点。
  • destroyOnClose:如果设置为 true,咱们将在面板敞开时销毁它的内容。
  • disabled:如果设置为 true,咱们将禁用面板,使其不能被关上或敞开。
  • forceRender:如果设置为 true,咱们将在面板敞开时依然渲染它的 DOM 构造。
  • key:这是面板的惟一标识符。
  • onClick:这是一个函数,它在面板的标题栏被点击时被触发。它接管一个参数,示意点击事件。
  • title:这是面板标题栏的内容。
import React, {useState, useEffect} from 'react';

const Panel = ({children, arrow, destroyOnClose, disabled, forceRender, key, onClick, title}) => {const [isOpen, setIsOpen] = useState(false);

  const handleClick = (event) => {if (disabled) return;
    setIsOpen(!isOpen);
    onClick && onClick(event);
  };

  const renderArrow = () => {if (typeof arrow === 'function') {return arrow(isOpen);
    }
    return arrow;
  };

  useEffect(() => {if (destroyOnClose && !isOpen) {children = null;}
  }, [isOpen]);

  return (<div key={key}>
      <button onClick={handleClick}>
        {title}
        {renderArrow()}
      </button>
      <div style={{display: isOpen || forceRender ? 'block' : 'none'}}>
        {children}
      </div>
    </div>
  );
};

const Collapse = ({children, accordion, activeKey, defaultActiveKey, onChange}) => {
};

Collapse.Panel = Panel;

export default Collapse;

forceRender 属性

咱们要增加一个名为 forceRender 的属性。如果这个属性被设置为 true,咱们会在组件暗藏时依然渲染 DOM 构造,如果面板渲染的数据量比拟大,这个属性特地有用,不会造成关上的时候会卡顿一下

import React, {useState} from 'react';

const Collapse = ({children, forceRender}) => {const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Collapse' : 'Expand'}
      </button>
      <div style={{display: isOpen || forceRender ? 'block' : 'none'}}>
        {children}
      </div>
    </div>
  );
};

export default Collapse;

实现折叠面板动画

height 形式实现

.collapse-panel {
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 10px;
  overflow: hidden;
}

.collapse-panel-button {
  background-color: #f5f5f5;
  color: #333;
  cursor: pointer;
  padding: 10px 15px;
  width: 100%;
  text-align: left;
  border: none;
  outline: none;
}

.collapse-panel-content {
  padding: 10px 15px;
  background-color: white;
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.2s ease-out;
}

.collapse-panel-content.open {max-height: 100vh;}
import React, {useState, useEffect, useRef} from 'react';

const Panel = ({children, arrow, destroyOnClose, disabled, forceRender, key, onClick, title}) => {const [isOpen, setIsOpen] = useState(false);
  const contentRef = useRef(null);

  const handleClick = (event) => {if (disabled) return;
    setIsOpen(!isOpen);
    onClick && onClick(event);
  };

  const renderArrow = () => {if (typeof arrow === 'function') {return arrow(isOpen);
    }
    return arrow;
  };

  useEffect(() => {if (destroyOnClose && !isOpen) {children = null;}
  }, [isOpen]);

  useEffect(() => {contentRef.current.style.maxHeight = isOpen ? `${contentRef.current.scrollHeight}px` : '0';
  }, [isOpen]);

  return (<div key={key} className="collapse-panel">
      <button onClick={handleClick} className="collapse-panel-button">
        {title}
        {renderArrow()}
      </button>
      <div ref={contentRef} className={`collapse-panel-content ${isOpen ? 'open' : ''}`}>
        {children}
      </div>
    </div>
  );
};

// ...

残缺成果:
jcode

其它形式

下面手风琴成果是利用 height 的实现,这种实现会触发重排,所以感兴趣的同学能够思考其它形式优化一下

  • 基于 scaleY
  • 应用 FLIP 技术增加动画优化
退出移动版