博客写的比较细致, 基本是一个小功能把
展示 实现 样式都粘贴到博客中了
, 一步步完成的, 想看实现可以直接看代码地址: Button & Checkbox
这次的目标是希望实现一个下面这样的
Table
组件, 看起来很美观, 功能基本满足日常开发, 后面我们会弄一个用例图, 一个个功能实现
- 基本的表格渲染
- 可以自由渲染数据
- 可以多选数据
- 可以排序
- 可以展开行
- etc...
在实现上面的功能之前, 我们先搭建开发环境, 完善基础组件
Button
和Checkbox
组件,
- 技术栈:
React + Hook + TS + Scss
- 环境:
Vite脚手架
搭建开发环境
Vite 是一种新型前端构建工具, 构建我们开发所需要的语言环境
如何使用 Vite
搭建 React + Ts
模板的脚手架
# vite 后面跟项目名称
# template 后面跟 需要的模板
yarn create vite 项目名称 --template react-ts
yarn create vite demo --template react-ts
# 进入项目 .scss .less .styl
yarn add sass/less/stylus # 内置了 css 预处理器
搭建我们的目录结构
.
├── App.scss
├── App.tsx // 展示组件
├── index.scss
├── lib // 组件源代码
│ └── Button
├── main.tsx // 入口
└── vite-env.d.ts
初始化项目 & Button 组件
- button 代码组织
// lib/Button/button.tsx
import { FC } from "react";
interface ButtonProps {}
const Button: FC<ButtonProps> = (props) => {
return <div>Button</div>;
};
export default Button;
- 显示组件
// App.tsx 显示组件样式
import { Button } from "./lib/index";
const App = () => {
return (
<div className="App">
<Button />
</div>
);
};
export default App;
Button用例图 & 使用Button
项目已经初始化, 我们思考一下用户如何使用我们的组件, 既简单又上手, 以及我们如何设计
props
, 可以让用户方便
- 有一个基础样式, 比默认的好看 => hover/focus/active 效果
- 可以有不同的类型展示
- 是否可点击
实现用例1: 默认按钮变成一个好看的按钮
// lib/Button/button.tsx
import { FC } from "react";
import "./button.scss";
interface ButtonProps {}
const Button: FC<ButtonProps> = (props) => {
return <button className="g-button g-button-default">按钮</button>;
};
export default Button;
.g-button {
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
border: none;
cursor: pointer;
&:focus {
outline: none;
}
// 默认样式
&.g-button-default {
color: #575757;
background: #f7f7fa;
&:hover {
background: #e5e5ea;
}
&:active {
background: #d9d9d9;
}
}
}
实现用例2: 按钮展示不同的类型
展示不同的类型, 其实就是添加不同的 class, 给元素不同的展现
import { Button } from "./lib/index";
const App = () => {
return (
<div className="App">
<Button />
<Button type="primary" />
<Button type="danger" />
</div>
);
};
export default App;
// 添加 type 属性
import { ButtonHTMLAttributes, FC } from "react";
import classnames from "classnames";
import "./button.scss";
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLElement>, "type"> {
type?: "primary" | "danger" | "default";
}
const Button: FC<ButtonProps> = (props) => {
const { type = "default", ...restProps } = props;
const classes = {
[`g-button-${type}`]: type,
};
return (
<button className={classnames("g-button", classes)} {...restProps}>
按钮
</button>
);
};
export default Button;
.g-button {
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
border: none;
cursor: pointer;
&:focus {
outline: none;
}
// 默认样式
&.g-button-default {
color: #575757;
background: #f7f7fa;
&:hover {
background: #e5e5ea;
}
&:active {
background: #d9d9d9;
}
}
// 主要颜色
&.g-button-primary {
color: #fff;
background: #3498ff;
&:hover {
background: #2589f5;
}
&:active {
background: #1675e0;
}
}
// 危险颜色
&.g-button-danger {
color: #fff;
background: #ff7875;
&:hover {
background: #e4383a;
}
&:active {
background: #d42926;
}
}
}
实现用例3: 按钮是否可点击
同理按钮是否可点击, 可以设置不同的样式, 并阻止点击事件触发
// props 的使用
import { Button } from "./lib/index";
import "./App.scss";
const App = () => {
return (
<div className="App">
<Button>普通按钮</Button>
<Button type="primary">主要按钮</Button>
<Button type="danger">危险按钮</Button>
<br />
<Button disabled>不可点击按钮</Button>
<Button type="primary" disabled>
不可点击主要按钮
</Button>
<Button type="danger" disabled>
不可点击危险按钮
</Button>
</div>
);
};
export default App;
// 实现 props disabled
import { ButtonHTMLAttributes, FC } from "react";
import classnames from "classnames";
import "./button.scss";
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLElement>, "type"> {
type?: "primary" | "danger" | "default";
}
const Button: FC<ButtonProps> = (props) => {
const { type = "default", disabled = false, children, ...restProps } = props;
// 加了一个 class 的判断
const cn = {
[`g-button-${type}`]: type,
[`g-button-disabled`]: disabled,
};
return (
<button className={classnames("g-button", cn)} {...restProps}>
{children}
</button>
);
};
export default Button;
.g-button {
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
border: none;
user-select: none;
cursor: pointer;
margin-right: 8px;
margin-top: 20px;
&:focus {
outline: none;
}
// 默认样式
&.g-button-default {
color: #575757;
background: #f7f7fa;
&:hover {
background: #e5e5ea;
}
&:active {
background: #d9d9d9;
}
&.g-button-disabled {
color: #c5c6c7;
pointer-events: none;
}
}
// 主要颜色
&.g-button-primary {
color: #fff;
background: #3498ff;
&:hover {
background: #2589f5;
}
&:active {
background: #1675e0;
}
&.g-button-disabled {
background: #cce9ff;
pointer-events: none;
}
}
// 危险颜色
&.g-button-danger {
color: #fff;
background: #ff7875;
&:hover {
background: #e4383a;
}
&:active {
background: #d42926;
}
&.g-button-disabled {
background: #eeb4b3;
pointer-events: none;
}
}
}
上面代码完成, 显示效果
实现一个 Checkbox 组件
首先在项目里面添加
Checkbox
文件夹
.
├── README.md
├── index.html
├── package.json
├── src
│ ├── App.scss
│ ├── App.tsx
│ ├── index.scss
│ ├── lib
│ │ ├── Button
│ │ ├── Checkbox
│ │ └── index.ts
│ ├── main.tsx
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock
用例图 & props 使用
- 一个基本好看的样式, 用户可以切换勾选
- 不可点击状态
- 触发事件通知外面
实现用例1: 好看的勾选框 & 可以切换状态
我们默认的勾选框比较小, 我们可以设置默认的不可见, 写一个勾选框来覆盖默认样式, 来模拟 checkbox 的行为, 为了点击文字也能触发 checkbox, 使用label 标签包裹元素, 并触发 change 事件, 改变 checkbox 的状态
import { FC, useState } from "react";
import classNames from "classnames";
import "./checkbox.scss";
interface CheckboxProps {
checked?: boolean;
}
const Checkbox: FC<CheckboxProps> = (props) => {
const { checked = false } = props;
// 当前是否被选中
const [currentChecked, setCurrentChecked] = useState(checked);
const classes = {
"g-checkbox-checked": currentChecked,
};
const handleChange = () => {
setCurrentChecked(!currentChecked);
};
return (
<label className="g-checkbox-wrapper">
<span className="g-checkbox">
<span className={classNames("g-checkbox-inner", classes)}></span>
<input
className="g-checkbox-input"
type="checkbox"
checked={currentChecked}
onChange={handleChange}
/>
</span>
<span className="g-checkobox-label">选择框</span>
</label>
);
};
export default Checkbox;
.g-checkbox-wrapper {
display: inline-flex;
align-items: center;
user-select: none;
cursor: pointer;
.g-checkbox {
padding: 10px;
&-inner {
position: relative;
display: block;
width: 16px;
height: 16px;
border: 1px solid #d9d9d9;
border-radius: 3px;
background: #fff;
transition: all 0.3s;
&.g-checkbox-checked {
background: #3498ff;
border: 1px solid #3498ff;
&::after {
content: "";
position: absolute;
top: 1px;
left: 5px;
width: 5px;
height: 10px;
transform: rotate(45deg);
border: 2px solid #fff;
border-top: none;
border-left: none;
}
}
}
&-input {
position: absolute;
opacity: 0;
box-sizing: border-box;
}
}
}
实现用例2: 不可点击状态
不可点击状态和 Button 类似, 加一个不可点击状态的样式, 不能触发 checkbox 的 change 事件
- 给父元素添加
disabled className
设置样式 -
change
事件disabled
不可触发 - 添加
checkbox value props
, 可选属性
// 示例
import { Button, Checkbox } from "./lib/index";
import "./App.scss";
const App = () => {
return (
<div className="App">
{/* 使用选择框 */}
<Checkbox value="选择框" />
<Checkbox disabled checked value="苹果" />
</div>
);
};
export default App;
// 如何实现
import { FC, useState } from "react";
import classNames from "classnames";
import "./checkbox.scss";
interface CheckboxProps {
checked?: boolean;
disabled?: boolean;
value?: string;
}
const Checkbox: FC<CheckboxProps> = (props) => {
const { checked = false, disabled = false, value = "" } = props;
// 当前是否被选中
const [currentChecked, setCurrentChecked] = useState(checked);
const classes_inner = {
"g-checkbox-checked": currentChecked,
};
const handleChange = () => {
// 如果 disabeld 不能触发 change 事件
if (disabled) return;
setCurrentChecked(!currentChecked);
};
return (
{/* 添加 disabled class */}
<label
className={classNames("g-checkbox-wrapper", {
"g-checkbox-disabled": disabled,
})}
>
<span className="g-checkbox">
<span className={classNames("g-checkbox-inner", classes_inner)}></span>
<input
className="g-checkbox-input"
type="checkbox"
checked={currentChecked}
onChange={handleChange}
value={value}
/>
</span>
<span className="g-checkbox-label">{value}</span>
</label>
);
};
export default Checkbox;
.g-checkbox-wrapper {
display: inline-flex;
align-items: center;
user-select: none;
cursor: pointer;
.g-checkbox {
padding: 10px;
&-inner {
position: relative;
display: block;
width: 16px;
height: 16px;
border: 1px solid #d9d9d9;
border-radius: 3px;
background: #fff;
transition: all 0.3s;
&.g-checkbox-checked {
background: #3498ff;
border: 1px solid #3498ff;
&::after {
content: "";
position: absolute;
top: 1px;
left: 5px;
width: 5px;
height: 10px;
transform: rotate(45deg);
border: 2px solid #fff;
border-top: none;
border-left: none;
}
}
}
&-input {
position: absolute;
opacity: 0;
box-sizing: border-box;
}
}
// disabled 样式
&.g-checkbox-disabled {
cursor: not-allowed;
.g-checkbox-inner {
background: #f7f7fa;
border: none;
&.g-checkbox-checked {
background: #cce9ff;
}
}
.g-checkbox-label {
color: #c5c6c7;
}
}
}
实现用例3: change事件回调
- 我们需要知道 checkbox 的状态, 传入 change事件
// 使用
import { Button, Checkbox } from "./lib/index";
import "./App.scss";
import { ChangeEvent } from "react";
const App = () => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// <input class="g-checkbox-input" type="checkbox" value="选择框">
// 选择框
// false/true
console.log(e.target, e.target.value, e.target.checked);
};
return (
<div className="App">
<Checkbox value="选择框" onChange={handleChange} />
<Checkbox disabled checked value="苹果" />
</div>
);
};
export default App;
// 实现
import { ChangeEvent, FC, useState } from "react";
import classNames from "classnames";
import "./checkbox.scss";
interface CheckboxProps {
checked?: boolean;
disabled?: boolean;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void; // 传参
}
const Checkbox: FC<CheckboxProps> = (props) => {
const { checked = false, disabled = false, value = "", onChange } = props;
// 当前是否被选中
const [currentChecked, setCurrentChecked] = useState(checked);
const classes_inner = {
"g-checkbox-checked": currentChecked,
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// 如果 disabeld 不能触发 change 事件
if (disabled) return;
setCurrentChecked(!currentChecked);
// 回调
onChange && onChange(e);
};
return (
<label
className={classNames("g-checkbox-wrapper", {
"g-checkbox-disabled": disabled,
})}
>
<span className="g-checkbox">
<span className={classNames("g-checkbox-inner", classes_inner)}></span>
<input
className="g-checkbox-input"
type="checkbox"
checked={currentChecked}
onChange={handleChange}
value={value}
/>
</span>
<span className="g-checkbox-label">{value}</span>
</label>
);
};
export default Checkbox;
- 有时候不知道这个元素的类型, 可以把鼠标放在元素上, 看一下什么类型
实现 CheckboxGroup 组件
平常我们使用多选框大部分是把多个选择框放在一起, 成为一组, 选择多个, CheckboxGroup 组件就是对 Checkbox 组件包装一下, 当触发 chnage 事件时, 知道我们选择了哪些选择框, 而不用给每一个 Checkbox 添加一个 change事件
用例图 & 使用方式
- 用户默认选中了哪几个
- 用户改变选中, 返回选中值
下面我们看一下如何让用户使用我们的组件, 第一种方案是 让用户写入每一个 Checkbox 组件, 第二种方案是让用户通过数据的方式我们自己来渲染这些Checkbox, 这次我先尝试使用 第一种方式来实现 CheckboxGroup
// 1. 元素方式
<CheckboxGroup selected={["11", "33"]} onChange={handleChange}>
<Checkbox value="11">苹果</Checkbox>
<Checkbox value="22">香蕉</Checkbox>
<Checkbox value="33" disabled>火龙果</Checkbox>
</CheckboxGroup>
// 2. 数据方式
const options = [
{
label: "电影",
value: "1",
disabled: true
},
{
label: "电视剧",
value: "2",
disabled: false
},
{
label: "做梦",
value: "3",
disabled: false
}
];
<CheckboxGroup options={options} selected={["1", "3"]} onChange={handleChange} />
- 第一种方式我们需要使用到
React.Children
这个 API, 我们来渲染我们的Children
,React.Children
提供了用于处理this.props.children
不透明数据结构的实用方法
// 如何使用最基本的
import { Button, Checkbox, CheckboxGroup } from "./lib/index";
import "./App.scss";
import { ChangeEvent } from "react";
const App = () => {
return (
<div className="App">
<CheckboxGroup>
<Checkbox value="苹果" />
<Checkbox value="香蕉" />
<Checkbox value="梨子" disabled />
</CheckboxGroup>
</div>
);
};
export default App;
// checkboxGroup.tsx
import React, { FC, ReactElement } from "react";
import Checkbox from "./checkbox";
interface GroupProps {
children: Array<ReactElement>;
}
const CheckboxGroup: FC<GroupProps> = (props) => {
const { children } = props;
const childWithProps = React.Children.map(children, (child, index) => {
// 确保每一个子元素都是 checkbox
if (child.type !== Checkbox) {
throw new Error("复选框组的子元素必须是 Checkbox");
}
// 返回每一个子元素并带有props
return React.cloneElement(child, {
...child.props,
key: index,
});
});
return <div>{childWithProps}</div>;
};
export default CheckboxGroup;
- 可以看到下面的我们可以正确渲染了
多选框组
, 但是我们不知道我们选中了哪一个, 如果有默认选中的我们也不知道怎么设置, 下面添加两个属性-
selected props
用户初始是否有默认选中的值 -
onChange
当用户触发子元素事件, 通知父元素选中一组中的哪几个
-
实现1: selected 数组
在父元素
CheckboxGroup
上添加<CheckboxGroup selected={["11", "33"]}>
时, 把selected 传递给Checkbox
, 初始时判断value 是否在selected
中, 如果在 checked 为 true, 不在 checked 为 false
// 使用 selected
import { Button, Checkbox, CheckboxGroup } from "./lib/index";
import "./App.scss";
import { ChangeEvent } from "react";
const App = () => {
return (
<div className="App" selected=["香蕉"]>
<CheckboxGroup>
<Checkbox value="苹果" />
<Checkbox value="香蕉" />
<Checkbox value="梨子" disabled />
</CheckboxGroup>
</div>
);
};
export default App;
// checkboxGroup.tsx
import React, { FC, ReactElement } from "react";
import Checkbox from "./checkbox";
interface GroupProps {
selected?: string[]; // group 使用, value 值的集合
children: Array<ReactElement>;
}
const CheckboxGroup: FC<GroupProps> = (props) => {
const { children, selected = [] } = props;
const childWithProps = React.Children.map(children, (child, index) => {
// 确保每一个子元素都是 checkbox
if (child.type !== Checkbox) {
throw new Error("复选框组的子元素必须是 Checkbox");
}
return React.cloneElement(child, {
...child.props,
key: index,
selected, // 添加的 selected props 传递给子元素
});
});
return <div>{childWithProps}</div>;
};
export default CheckboxGroup;
// checkbox.tsx
import { ChangeEvent, FC, useEffect, useState } from "react";
import classNames from "classnames";
import "./checkbox.scss";
interface CheckboxProps {
checked?: boolean;
disabled?: boolean;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
// group 传的 props
selected?: string[];
}
const Checkbox: FC<CheckboxProps> = (props) => {
const {
selected = [],
checked = false,
disabled = false,
value = "",
onChange,
} = props;
// 当前是否被选中
const [currentChecked, setCurrentChecked] = useState(checked);
// + 初始判断是否被选中
useEffect(() => {
if (selected.length > 0 && selected.indexOf(value) > -1) {
setCurrentChecked(true);
}
}, []);
const classes_inner = {
"g-checkbox-checked": currentChecked,
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// 如果 disabeld 不能触发 change 事件
if (disabled) return;
setCurrentChecked(!currentChecked);
onChange && onChange(e);
};
return (
<label
className={classNames("g-checkbox-wrapper", {
"g-checkbox-disabled": disabled,
})}
>
<span className="g-checkbox">
<span className={classNames("g-checkbox-inner", classes_inner)}></span>
<input
className="g-checkbox-input"
type="checkbox"
checked={currentChecked}
onChange={handleChange}
value={value}
/>
</span>
<span className="g-checkbox-label">{value}</span>
</label>
);
};
export default Checkbox;
实现2: change事件
上面我们实现了 selected, 现在当我们改变选中数组时告诉父元素哪几个兄弟被选中了, 所以我们需要根据 selected 的值来为初始值, 当改变子元素的 checked, 来改变选中的值
// 使用 onChange
import { Button, Checkbox, CheckboxGroup } from "./lib/index";
import "./App.scss";
import { ChangeEvent } from "react";
const App = () => {
const handleChange = (values: string[]) => {
setValues(values);
};
return (
<div className="App" selected=["香蕉"] onChange={handleChange}>
<CheckboxGroup>
<Checkbox value="苹果" />
<Checkbox value="香蕉" />
<Checkbox value="梨子" disabled />
</CheckboxGroup>
</div>
);
};
export default App;
// CheckboxGroup.tsx
import React, {
ChangeEvent,
FC,
ReactElement,
useEffect,
useState,
} from "react";
import Checkbox from "./checkbox";
interface GroupProps {
selected?: string[]; // group 使用, value 值的集合
children: Array<ReactElement>;
onChange?: (selected: string[]) => void;
}
const CheckboxGroup: FC<GroupProps> = (props) => {
const { children, selected = [], onChange } = props;
const [selectedValue, setSelectedValue] = useState(selected);
// 变化时改变 值
const handleGroupChange = (e: ChangeEvent<HTMLInputElement>) => {
const { checked, value } = e.currentTarget;
if (checked) {
setSelectedValue([...selectedValue, value]);
} else {
setSelectedValue((arr) => arr.filter((i) => i !== value));
}
};
// 值每次变化都暴露出去
useEffect(() => {
onChange && onChange(selectedValue);
}, [selectedValue]);
const childWithProps = React.Children.map(children, (child, index) => {
// 确保每一个子元素都是 checkbox
if (child.type !== Checkbox) {
throw new Error("复选框组的子元素必须是 Checkbox");
}
return React.cloneElement(child, {
...child.props,
key: index,
selected,
onChange: handleGroupChange, // 利用回调
});
});
return <div>{childWithProps}</div>;
};
export default CheckboxGroup;
上面完善了 Button组件和Checkbox组件的基本使用, 可以在后面我们写Table 组件的时候直接使用, 如果需要其它功能, 自己添加一下 props