这个React响应式Tag列表,不点进来批评批评吗

前言

之前写过一篇相关的文章,但是没啥流量(没办法,不当标题党都没人点进来看😂),最近因为有了新需求于是在功能和写法上都进行了优化,需要对比的同学可以看一下上篇文章,上篇文章在这里.

大致需求呢就是有一个tag列表,只需要展示2行,当tag数量超出规定行数之后进行省略,鼠标移上去展示省略的内容 还是老规矩,先看一下最终的实现效果:

实现过程

第一步

第一步也是最核心的问题,就是需要计算出在第几个tag处进行隐藏,核心代码就是下面这段,原理就是通过useLayoutEffect拿到dom列表进行遍历,如果某个tag的offsetTop大于 行数*tag高度 了就表示超出了规定行数

js 复制代码
useLayoutEffect(() => {
    if (!ref.current) return;
    const { childNodes, offsetWidth } = ref.current! as HTMLDivElement;
    const nodeArr = Array.from(childNodes) as HTMLSpanElement[];
    let index = nodeArr?.findIndex((item) => {
      return item.offsetTop > item.offsetHeight * row;
    });
    if (
      index > 0 &&
      offsetWidth -
        nodeArr[index - 1].offsetLeft -
        nodeArr[index - 1].offsetWidth <
        50
    ) {
      index--;
    }
    setIndex(index);
}, []); 

当然光计算出位置是不够的,因为有可能出现下面这种情况,虽然我们算出是在第3个tag换行了,但实际上第一行剩下的宽度不足以放下省略按钮,所以我们还需要计算下当前行的剩余宽度,如果不够需要index-1

计算剩余宽度前:

计算剩余宽度后:

至于省略按钮之前的那一版我是通过计算位置,然后绝对定位来实现的,这种方式不太好,因为需要在外部的box上预留出50的padding-right(防止定位重叠了),所以这次优化了一下,通过计算出的index来截取tag数组,在后面追加一个tag

老版的实现:

响应式

响应式的实现原理和之前那篇差不多,都是通过'resize-observer-polyfill'这个库来监听元素改变,重新计算位置,只不过这一次优化了一下写法

老版本的话省略的dom都是实际存在的,因为每次渲染都需要遍历dom元素来计算位置,如果截取的话,就会导致组件刷新时获取的dom元素数量不对

在这次的版本中为了解决这个问题,我在原本的组件外包裹了一层,通过父组件改变key来重新挂载子组件,在子组件中监听组件第一次挂载就可以计算出位置了,每次需要重新计算index时都重新挂载一次

js 复制代码
父组件:
const ref = useRef<HTMLDivElement>(null);
const [renderKey, setRenderKey] = useState(Date.now());
useLayoutEffect(() => {
if (!ref.current) return;
const observe = new ResizeObserver(
  throttle(() => {
    setRenderKey(Date.now());
  }, 300),
);
observe.observe(ref.current);
return () => {
  observe.disconnect();
};
}, []);
useEffect(() => {
setRenderKey(Date.now());
}, [data, row, className]); // 控制tagList强制刷新,以便于重新计算位置
return (
<StyledWrap ref={ref}>
  <TagContent {...{ data, row, className, onClick }} key={renderKey} />
</StyledWrap>
);

子组件:
useLayoutEffect(() => {
if (!ref.current) return;
const { childNodes, offsetWidth } = ref.current! as HTMLDivElement;
const nodeArr = Array.from(childNodes) as HTMLSpanElement[];
let index = nodeArr?.findIndex((item) => {
  return item.offsetTop > item.offsetHeight * row;
});
console.log('没有计算剩余宽度的index:', index);
if (
  index > 0 &&
  offsetWidth -
    nodeArr[index - 1].offsetLeft -
    nodeArr[index - 1].offsetWidth <
    50
) {
  index--;
}
console.log('计算了剩余宽度的index:', index);
setIndex(index);
}, []); // 因为父组件控制了强制渲染,所以只需要在第一次渲染后计算位置就可以了

这样做的目的就是为了我们只需要在组件初始化的时候计算位置,每次更新位置时都重新挂载组件,然后截取tag数组,可以看到新写法里面已经根据index做了截取

完整代码在这里

我目前想到的实现方法就是这样,如果各位大佬有更好的实现方式,欢迎讨论👍!

完整代码在下面

js 复制代码
import { Popover } from 'antd';
import { throttle } from 'lodash-es';
import React, {
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
interface IOption {
  label: string;
  value: string | number;
}

interface IProps {
  row?: number;
  data: IOption[];
  className?: string;
  onClick?: (item: IOption) => void;
}

const TagContent = ({ row = 1, data, className, onClick }: IProps) => {
  const ref = useRef<HTMLDivElement>(null);
  const [index, setIndex] = useState(-1);
  useLayoutEffect(() => {
    if (!ref.current) return;
    const { childNodes, offsetWidth } = ref.current! as HTMLDivElement;
    const nodeArr = Array.from(childNodes) as HTMLSpanElement[];
    let index = nodeArr?.findIndex((item) => {
      return item.offsetTop > item.offsetHeight * row;
    });
    if (
      index > 0 &&
      offsetWidth -
        nodeArr[index - 1].offsetLeft -
        nodeArr[index - 1].offsetWidth <
        50
    ) {
      index--;
    }
    setIndex(index);
  }, []); // 因为父组件控制了强制渲染,所以只需要在第一次渲染后计算位置就可以了
  const onItemClick = (item: IOption) => {
    if (!onClick) return undefined;
    return () => {
      onClick(item);
    };
  };
  const { showList, rest } = useMemo(() => {
    if (index > -1) {
      return {
        showList: data.slice(0, index),
        rest: data.slice(index, data.length),
      };
    }
    return {
      showList: data,
      rest: [],
    };
  }, [index, data]);
  return (
    <StyledTagList className={className} ref={ref}>
      {showList.map((item) => (
        <StyledTag
          key={item.value}
          onClick={onItemClick(item)}
          title={item.label}
        >
          {item.label}
        </StyledTag>
      ))}
      {index > -1 && (
        <Popover
          overlayClassName="toggle-tag-list"
          content={
            <>
              {rest.map((item) => (
                <StyledTag
                  key={item.value}
                  onClick={onItemClick(item)}
                  title={item.label}
                >
                  {item.label}
                </StyledTag>
              ))}
            </>
          }
        >
          <StyledTag style={{ cursor: 'pointer' }}>
            +{data.length - index}
          </StyledTag>
        </Popover>
      )}
    </StyledTagList>
  );
};

export const TagList = ({ data, row, className, onClick }: IProps) => {
  const ref = useRef<HTMLDivElement>(null);
  const [renderKey, setRenderKey] = useState(Date.now());
  useLayoutEffect(() => {
    if (!ref.current) return;
    const observe = new ResizeObserver(
      throttle(() => {
        setRenderKey(Date.now());
      }, 300),
    );
    observe.observe(ref.current);
    return () => {
      observe.disconnect();
    };
  }, []);
  useEffect(() => {
    setRenderKey(Date.now());
  }, [data, row, className]); // 控制tagList强制刷新,以便于重新计算位置
  return (
    <StyledWrap ref={ref}>
      <TagContent {...{ data, row, className, onClick }} key={renderKey} />
    </StyledWrap>
  );
};
const StyledWrap = styled.div`
  position: relative;
  overflow: hidden;
  height: 100%;
`;

const StyledTagList = styled.div`
  position: absolute;
  width: 100%;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
`;
const StyledTag = styled.span<{ onClick?: () => void }>`
  min-width: 20px;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-align: center;
  padding: 0px 6px;
  border-radius: 2px;
  background: rgba(63, 81, 181, 0.08);
  color: #1f2878;
  line-height: 20px;
  white-space: nowrap;
  ${({ onClick }) =>
    !onClick
      ? ''
      : css`
          cursor: pointer;
        `}
`;
相关推荐
大怪v1 小时前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范
狂炫冰美式1 小时前
不谈技术,搞点文化 🧀 —— 从复活一句明代残诗破局产品迭代
前端·人工智能·后端
xw52 小时前
npm几个实用命令
前端·npm
!win !2 小时前
npm几个实用命令
前端·npm
代码狂想家2 小时前
使用openEuler从零构建用户管理系统Web应用平台
前端
dorisrv4 小时前
优雅的React表单状态管理
前端
蓝瑟4 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv4 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱4 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder5 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端