likes
comments
collection
share

CSS展开收起JS版

作者站长头像
站长
· 阅读数 50

前言

上一篇文章采用了纯CSS方案实现展开收起,本篇采用JS方案实现该功能

优点:在不需要折叠时,直接使展开按钮消失,而不是一直存在,这样就不会存在占位的问题,解决了CSS方案的痛点

缺点:消耗部分性能

效果:

CSS展开收起JS版

一、创建计算高度的hook

使用JS方案的关键点在于计算内容的高度是否大于预设高度,如果大于说明需要折叠,在数据中增加标识并使页面重渲染进行更新

由于业务情景是表格内单元格,所以需要读取多个单元格的高度,因此先使用ref处理数据,处理完后再使用setState进行批量更新

const listRef = useRef(tableData)

const autoSize = useCallback(() => {
//读取类为content的DOM
  tblRef.current?.querySelectorAll(".content").forEach((el, i) => {
  //判断每个单元格的高度是否大于60,大于说明需要折行,添加属性more为true
    listRef.current[i].more = el.scrollHeight > 60 ? true : false;
  });
 //进行批量更新
  setTableData(() => [...listRef.current]);
}, [tblRef, setTableData]);

由于读取的是接口数据,所有开始的tableData和listRef.current都是空数组,因此需要使用hook进行数据监听

const [renderCtrl, setRenderCtrl] = useState(0);

useEffect(() => {
  //当获取到接口数据,tableData更新了,因此与istRef.current的值不同,使renderCtrl+1
  if (!isEqual(listRef.current, tableData)) setRenderCtrl((num) => ++num);
  //将表格数据赋值到listRef.current
  listRef.current = tableData;
}, [tableData, setRenderCtrl]);

useLayoutEffect(() => {
  autoSize();
  //这里监听renderCtrl属性,当更新时重新触发autoSize函数
}, [renderCtrl, autoSize]);

hook的核心代码已实现,这里贴一个完整的hook代码

新建useLimitLine.js

import {
  useEffect,
  useCallback,
  useRef,
  useState,
  useLayoutEffect,
} from "react";
import { isEqual } from "lodash";
import { useDebounceFn } from "ahooks";

const useLimitLine = (tableData, setTableData, tblRef) => {
  const listRef = useRef(tableData);
  const [renderCtrl, setRenderCtrl] = useState(0);

  const autoSize = useCallback(() => {
    tblRef.current?.querySelectorAll(".content").forEach((el, i) => {
      listRef.current[i].more = el.scrollHeight > 60 ? true : false;
    });

    setTableData(() => [...listRef.current]);
  }, [tblRef, setTableData]);

  const { run } = useDebounceFn(autoSize);

  useEffect(() => {
    if (!isEqual(listRef.current, tableData)) setRenderCtrl((num) => ++num);

    listRef.current = tableData;
  }, [tableData, setRenderCtrl]);

  useLayoutEffect(() => {
    autoSize();
  }, [renderCtrl, autoSize]);

  useEffect(() => {
    window.addEventListener("resize", run, false);
    return () => {
      window.removeEventListener("resize", run, false);
    };
  }, [run]);

  return {};
};

export default useLimitLine;

二、创建展开收起的组件

关键点:组件不能使用React.memo进行包裹,因为React.memo会进行浅比较,当对象内部属性更新时,不会触发组件的更新效果

新建一个title.js组件

import { useState } from "react";
import styled from "styled-components";
import cn from "classnames";

//组件不能使用memo进行浅比较,必须每次返回最新的
const TitlePopover = ({ text, obj, setTableData }) => {
  //checked用来判断是展开状态还是收起状态
  //obj.more用来判断单元格是否需要折行
  const [checked, setChecked] = useState(false);
  return (
    <Wrap>
      <div className={cn({ open: checked }, "content")}>
        <div
          className={cn({ btnClose: !obj.more }, { btnOpen: !checked }, "more")}
          onClick={() => {
            setChecked((prev) => !prev);
          }}
        >
          {checked ? "收起" : "展开"}
        </div>
        <span>{text}</span>
      </div>
    </Wrap>
  );
};

export default TitlePopover;

const Wrap = styled.div`
  display: flex;
  .content {
    max-height: 60px;
    overflow: hidden;
    line-height: 20px;
    position: relative;
    text-align: justify;
    &::before {
      content: "";
      height: calc(100% - 20px);
      float: right;
    }
    &:hover {
      text-decoration: underline;
    }
  }

  .open {
    max-height: none;
  }

  .btnClose {
    display: none;
  }

  .more {
    font-size: 13px;
    color: #025cdc;
    line-height: 20px;
    cursor: pointer;
    width: 26px;
    float: right;
    clear: both;
    position: relative;
  }
  .btnOpen {
    margin-left: 15px;
    &::before {
      content: "...";
      position: absolute;
      left: -5px;
      color: #333;
      transform: translateX(-100%);
    }
  }
`;

三、在表格中调用

同样的关键点在于表格的columns不能用useMemo包裹,每次返回最新的

修改App.js

import React, { useRef, useState, useEffect } from "react";
import { Table } from "antd";
import useLimitLine from "./useLimitLine";
import Title from "./title";

//模拟接口
const getData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        {
          key: "1",
          name: "John Brown",
          age: 32,
          address:
            "互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)",
          more: false,
        },
        {
          key: "2",
          name: "Jim Green",
          age: 42,
          address:
            "互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)",
          more: false,
        },
        {
          key: "3",
          name: "Joe Black",
          age: 32,
          address:
            "计算机软、硬件的开发;计算机系统服务;销售计算机软、硬件及辅助设备",
          more: false,
        },
      ]);
    }, 200);
  });
};

function App() {
  const tblRef = useRef();
  const [tableData, setTableData] = useState([]);
  useLimitLine(tableData, setTableData, tblRef);

  useEffect(() => {
    console.log("tableData", tableData);
  }, [tableData]);

  useEffect(() => {
    getData().then((res) => {
      setTableData(res);
    });
  }, []);

  //columns不能用useMemo包裹,每次返回最新的
  const columns = [
    {
      title: "Name",
      dataIndex: "name",
      key: "name",
    },
    {
      title: "Age",
      dataIndex: "age",
      key: "age",
    },
    {
      title: "Address",
      dataIndex: "address",
      key: "address",
      render: (text, obj) => (
        <Title text={text} obj={obj} setTableData={setTableData} />
      ),
    },
  ];

  return (
    <div className="container" ref={tblRef}>
      <Table columns={columns} dataSource={tableData} bordered />
    </div>
  );
}

export default App;

四、加个tooltip效果

当单元格需要折行并且为折叠状态时,使用Popover组件

import { useState } from "react";
import styled from "styled-components";
import cn from "classnames";
import { Popover } from "antd";

//组件不能使用memo进行浅比较,必须每次返回最新的
const TitlePopover = ({ text, obj, setTableData }) => {
  const [checked, setChecked] = useState(false);

  return (
    <Wrap>
      {obj.more && !checked ? (
        <Popover content={text} placement="top">
          <div className={cn({ open: checked }, "content")}>
            <div
              className={cn(
                { btnClose: !obj.more },
                { btnOpen: !checked },
                "more"
              )}
              onClick={() => {
                setChecked((prev) => !prev);
              }}
            >
              {checked ? "收起" : "展开"}
            </div>
            <span>{text}</span>
          </div>
        </Popover>
      ) : (
        <div className={cn({ open: checked }, "content")}>
          <div
            className={cn(
              { btnClose: !obj.more },
              { btnOpen: !checked },
              "more"
            )}
            onClick={() => {
              setChecked((prev) => !prev);
            }}
          >
            {checked ? "收起" : "展开"}
          </div>
          <span>{text}</span>
        </div>
      )}
    </Wrap>
  );
};

export default TitlePopover;

const Wrap = styled.div`
  display: flex;
  .content {
    max-height: 60px;
    overflow: hidden;
    line-height: 20px;
    position: relative;
    text-align: justify;
    &::before {
      content: "";
      height: calc(100% - 20px);
      float: right;
    }
    &:hover {
      text-decoration: underline;
    }
  }

  .open {
    max-height: none;
  }

  .btnClose {
    display: none;
  }

  .more {
    font-size: 13px;
    color: #025cdc;
    line-height: 20px;
    cursor: pointer;
    width: 26px;
    float: right;
    clear: both;
    position: relative;
  }
  .btnOpen {
    margin-left: 15px;
    &::before {
      content: "...";
      position: absolute;
      left: -5px;
      color: #333;
      transform: translateX(-100%);
    }
  }
`;

五、结语

全部代码以及展示效果都放到上面了,如果有不理解或者没有实现该效果的,可以先看我上篇文章 4千字CSS文本展开收起详细教程

转载自:https://juejin.cn/post/7133866063778807839
评论
请登录