这个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;
        `}
`;
相关推荐
喵叔哟7 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django