react的响应式容器组件-基于容器的宽度来决定显示的子元素数量

背景

最近接到了一个需求,基于容器的宽度来决定显示的子元素数量。如果屏幕或容器足够宽,它将展示更多的子元素;如果屏幕较窄,它将隐藏一些子元素,并提供一个下拉菜单来查看这些被隐藏的内容

本来想偷懒,用gpt来做嘛,但是gpt做出来的组件库对于功能的满足不能说十全十美吧,只能一无是处,试了两个小时,放弃了,还不如自己写呢

说干就干!!!

实现思路

  1. 组件属性定义&初始化状态
  2. 动态计算和更新可见子元素数量
  3. 窗口尺寸变化的处理
  4. 渲染可见和隐藏的子元素
  5. 组件的最终渲染

既然有了实现思路我们就开始实现吧

实现

组件属性定义&初始化状态

  • 组件属性定义ResponsiveContainer 组件接收两个属性:childrenlimitchildren 是任意的 React 节点,而 limit 是一个可选属性,用于限制显示子元素的数量。如果没有给定 limit,组件将基于容器的宽度动态确定可见的子元素数量。
  • 初始化状态 : 组件内部使用 useRef 创建两个引用:containerRefshadowRefcontainerRef 用于引用实际显示内容的容器,而 shadowRef 用于临时存放所有子元素以计算它们的总宽度。同时,使用 useState 创建一个 visibleCount 状态,用来存储应该显示的子元素数量。

组件属性定义

kotlin 复制代码
interface ResponsiveContainerProps {
  children: ReactNode; // 期望传入任意的React节点作为children
  limit?: number; // 可选属性,限制显示的子元素数量;如果未设置,将基于容器宽度动态决定
}

初始化状态

ini 复制代码
  const containerRef = useRef<HTMLDivElement>(null);
  const [visibleCount, setVisibleCount] = useState(0);
  const shadowRef = useRef<HTMLDivElement>(null);

动态计算和更新可见子元素数量

updateChildrenVisibility 函数负责计算在当前容器宽度下,可以显示多少子元素。如果提供了 limit 属性,则直接使用 limit 设定的值更新 visibleCount 状态。如果没有提供 limit,函数将遍历 shadowRef 中的子元素,累加它们的宽度,直到达到 containerRef 容器的宽度限制。这个过程中还考虑了额外预留的空间(如12像素),确保内容不会溢出容器。

ini 复制代码
  // 更新子元素的可见性
  const updateChildrenVisibility = useCallback(() => {
    // 如果limit有值,将可见子元素数量设置为limit,否则,根据容器的宽度和子元素的宽度计算可见子元素数量
    if (limit !== undefined && limit !== null) {
      setVisibleCount(limit);
    } else {
      if (shadowRef.current && containerRef.current) {
        // 累加宽度初始化为0
        let currentWidth = 0;
        // 获取容器的最大宽度
        const { width: maxWidth } =
          containerRef.current.getBoundingClientRect();
        const count = shadowRef.current.childElementCount;
        const breadcrumbChildren = shadowRef.current.children;

        let localCount = 0;
        for (let i = 0; i < count; i++) {
          const child = breadcrumbChildren[i];
          const { width } = child.getBoundingClientRect();
          // 如果当前宽度加上当前子元素的宽度超过了容器的最大宽度就结束循环
          if (maxWidth - 12 <= currentWidth + width) {
            break;
          } else {
            //否则,根据容器的宽度和子元素的宽度计算可见子元素数量
            currentWidth += width;
            localCount += 1;
          }
        }
        setVisibleCount(localCount);
      }
    }
  }, [limit]);

窗口尺寸变化的处理

使用 useEffect 钩子监听窗口大小的变化。当窗口大小改变时,调用 updateChildrenVisibility 函数来重新计算可见的子元素数量。这样确保了响应式行为,能适应窗口或容器宽度的变更。

scss 复制代码
  // 使用useEffect来处理窗口尺寸变化事件
  useEffect(() => {
    updateChildrenVisibility();
    const handleResize = () => updateChildrenVisibility();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [children, limit]);

渲染可见和隐藏的子元素

组件使用 React.Children.toArraychildren 转换为数组,并根据 visibleCount 状态来拆分为两个数组:visibleChildrenhiddenChildrenvisibleChildren 包含那些将会被直接渲染在容器中的子元素,对于 hiddenChildren,组件使用 Dropdown 菜单来提供访问隐藏子元素的方式。当用户与 Dropdown 交互时(例如鼠标悬停),隐藏的子元素将作为下拉菜单项显示出来。

ini 复制代码
  const visibleChildren = React.Children.toArray(children).slice(
    0,
    visibleCount,
  );
  const hiddenChildren = React.Children.toArray(children).slice(visibleCount);
  // 准备Dropdown菜单内容
  const menu = (
    <Menu>
      {hiddenChildren.map((child, index) => (
        <Menu.Item key={index}>{child}</Menu.Item>
      ))}
    </Menu>
  );

最终渲染

组件返回了一个包裹 wrappershadow 两部分的 JSX 结构。wrapper 用于显示可见的子元素,而 shadow(虽然被设置为不可见)用于帮助计算子元素的宽度。Dropdown 菜单被放置在 wrapper 中,以便用户可以交互。

必须将visibility: hidden,而不是使用display: none。当我们给一个元素设置display: none时,这个元素会从文档流中完全消失。这意味着它不占据任何空间,也不会渲染任何内容,因此无法进行宽度测量

less 复制代码
 return (
    <div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
      <div
        className="wrapper"
        style={{
          display: 'flex',
          overflow: 'hidden',
          flexWrap: 'nowrap',
          width: '100%',
        }}
      >
        {visibleChildren.map((child, index) => (
          <div key={index} style={{ display: 'inline-block' }}>
            {child}
          </div>
        ))}
        {hiddenChildren.length > 0 && (
          <Dropdown overlay={menu} trigger={['hover']} arrow>
            <EllipsisOutlined style={{ fontSize: 16 }} />
          </Dropdown>
        )}
      </div>
      <div
        className="shadow"
        ref={shadowRef}
        style={{
          position: 'absolute',
          zIndex: -1,
          visibility: 'hidden',
          flexWrap: 'nowrap',
          display: 'flex',
        }}
      >
        {children}
      </div>
    </div>
  );

所有代码

ini 复制代码
import { Dropdown, Menu } from 'antd';
import React, {
  ReactNode,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import { EllipsisOutlined } from '@ant-design/icons';

// 定义组件的Props类型
interface ResponsiveContainerProps {
  children: ReactNode; // 期望传入任意的React节点作为children
  limit?: number; // 可选属性,限制显示的子元素数量;如果未设置,将基于容器宽度动态决定
}

const ResponsiveContainer: React.FC<ResponsiveContainerProps> = ({
  children,
  limit,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [visibleCount, setVisibleCount] = useState(0);
  const shadowRef = useRef<HTMLDivElement>(null);

  // 更新子元素的可见性
  const updateChildrenVisibility = useCallback(() => {
    if (limit !== undefined && limit !== null) {
      setVisibleCount(limit);
    } else {
      if (shadowRef.current && containerRef.current) {
        let currentWidth = 0;
        const { width: maxWidth } =
          containerRef.current.getBoundingClientRect();
        const count = shadowRef.current.childElementCount;
        const breadcrumbChildren = shadowRef.current.children;

        let localCount = 0;
        for (let i = 0; i < count; i++) {
          const child = breadcrumbChildren[i];
          const { width } = child.getBoundingClientRect();
          if (maxWidth - 12 <= currentWidth + width) {
            break;
          } else {
            currentWidth += width;
            localCount += 1;
          }
        }
        setVisibleCount(localCount);
      }
    }
  }, [limit]);


  // 使用useEffect来处理窗口尺寸变化事件
  useEffect(() => {
    updateChildrenVisibility();
    const handleResize = () => updateChildrenVisibility();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [children, limit]);

  const visibleChildren = React.Children.toArray(children).slice(
    0,
    visibleCount,
  );
  const hiddenChildren = React.Children.toArray(children).slice(visibleCount);
  // 准备Dropdown菜单内容
  const menu = (
    <Menu>
      {hiddenChildren.map((child, index) => (
        <Menu.Item key={index}>{child}</Menu.Item>
      ))}
    </Menu>
  );

  return (
    <div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
      <div
        className="wrapper"
        style={{
          display: 'flex',
          overflow: 'hidden',
          flexWrap: 'nowrap',
          width: '100%',
        }}
      >
        {visibleChildren.map((child, index) => (
          <div key={index} style={{ display: 'inline-block' }}>
            {child}
          </div>
        ))}
        {hiddenChildren.length > 0 && (
          <Dropdown overlay={menu} trigger={['hover']} arrow>
            <EllipsisOutlined style={{ fontSize: 16 }} />
          </Dropdown>
        )}
      </div>
      <div
        className="shadow"
        ref={shadowRef}
        style={{
          position: 'absolute',
          zIndex: -1,
          visibility: 'hidden',
          flexWrap: 'nowrap',
          display: 'flex',
        }}
      >
        {children}
      </div>
    </div>
  );
};

参考

相关推荐
她似晚风般温柔7892 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
Jiaberrr3 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy3 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白3 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、3 小时前
Web Worker 简单使用
前端
web_learning_3213 小时前
信息收集常用指令
前端·搜索引擎
Ylucius3 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
tabzzz3 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百4 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao4 小时前
自动化测试常用函数
前端·css·html5