CSS展开收起JS版
前言
上一篇文章采用了纯CSS方案实现展开收起,本篇采用JS方案实现该功能
优点:在不需要折叠时,直接使展开按钮消失,而不是一直存在,这样就不会存在占位的问题,解决了CSS方案的痛点
缺点:消耗部分性能
效果:
一、创建计算高度的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