博客寫的比較細(xì)致, 基本是一個小功能把
展示 實(shí)現(xiàn) 樣式都粘貼到博客中了
, 一步步完成的, 想看實(shí)現(xiàn)可以直接看代碼地址: Button & Checkbox
這次的目標(biāo)是希望實(shí)現(xiàn)一個下面這樣的
Table
組件, 看起來很美觀, 功能基本滿足日常開發(fā), 后面我們會弄一個用例圖, 一個個功能實(shí)現(xiàn)
- 基本的表格渲染
- 可以自由渲染數(shù)據(jù)
- 可以多選數(shù)據(jù)
- 可以排序
- 可以展開行
- etc...
在實(shí)現(xiàn)上面的功能之前, 我們先搭建開發(fā)環(huán)境, 完善基礎(chǔ)組件
Button
和Checkbox
組件,
- 技術(shù)棧:
React + Hook + TS + Scss
- 環(huán)境:
Vite腳手架
搭建開發(fā)環(huán)境
Vite 是一種新型前端構(gòu)建工具, 構(gòu)建我們開發(fā)所需要的語言環(huán)境
如何使用 Vite
搭建 React + Ts
模板的腳手架
# vite 后面跟項(xiàng)目名稱
# template 后面跟 需要的模板
yarn create vite 項(xiàng)目名稱 --template react-ts
yarn create vite demo --template react-ts
# 進(jìn)入項(xiàng)目 .scss .less .styl
yarn add sass/less/stylus # 內(nèi)置了 css 預(yù)處理器
搭建我們的目錄結(jié)構(gòu)
.
├── App.scss
├── App.tsx // 展示組件
├── index.scss
├── lib // 組件源代碼
│ └── Button
├── main.tsx // 入口
└── vite-env.d.ts
初始化項(xiàng)目 & 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
項(xiàng)目已經(jīng)初始化, 我們思考一下用戶如何使用我們的組件, 既簡單又上手, 以及我們?nèi)绾卧O(shè)計(jì)
props
, 可以讓用戶方便
- 有一個基礎(chǔ)樣式, 比默認(rèn)的好看 => hover/focus/active 效果
- 可以有不同的類型展示
- 是否可點(diǎn)擊
實(shí)現(xiàn)用例1: 默認(rèn)按鈕變成一個好看的按鈕
// 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;
}
// 默認(rèn)樣式
&.g-button-default {
color: #575757;
background: #f7f7fa;
&:hover {
background: #e5e5ea;
}
&:active {
background: #d9d9d9;
}
}
}
實(shí)現(xiàn)用例2: 按鈕展示不同的類型
展示不同的類型, 其實(shí)就是添加不同的 class, 給元素不同的展現(xiàn)
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;
}
// 默認(rèn)樣式
&.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;
}
}
// 危險(xiǎn)顏色
&.g-button-danger {
color: #fff;
background: #ff7875;
&:hover {
background: #e4383a;
}
&:active {
background: #d42926;
}
}
}
實(shí)現(xiàn)用例3: 按鈕是否可點(diǎn)擊
同理按鈕是否可點(diǎn)擊, 可以設(shè)置不同的樣式, 并阻止點(diǎn)擊事件觸發(fā)
// props 的使用
import { Button } from "./lib/index";
import "./App.scss";
const App = () => {
return (
<div className="App">
<Button>普通按鈕</Button>
<Button type="primary">主要按鈕</Button>
<Button type="danger">危險(xiǎn)按鈕</Button>
<br />
<Button disabled>不可點(diǎn)擊按鈕</Button>
<Button type="primary" disabled>
不可點(diǎn)擊主要按鈕
</Button>
<Button type="danger" disabled>
不可點(diǎn)擊危險(xiǎn)按鈕
</Button>
</div>
);
};
export default App;
// 實(shí)現(xiàn) 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;
}
// 默認(rèn)樣式
&.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;
}
}
// 危險(xiǎn)顏色
&.g-button-danger {
color: #fff;
background: #ff7875;
&:hover {
background: #e4383a;
}
&:active {
background: #d42926;
}
&.g-button-disabled {
background: #eeb4b3;
pointer-events: none;
}
}
}
上面代碼完成, 顯示效果
實(shí)現(xiàn)一個 Checkbox 組件
首先在項(xiàng)目里面添加
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 使用
- 一個基本好看的樣式, 用戶可以切換勾選
- 不可點(diǎn)擊狀態(tài)
- 觸發(fā)事件通知外面
實(shí)現(xiàn)用例1: 好看的勾選框 & 可以切換狀態(tài)
我們默認(rèn)的勾選框比較小, 我們可以設(shè)置默認(rèn)的不可見, 寫一個勾選框來覆蓋默認(rèn)樣式, 來模擬 checkbox 的行為, 為了點(diǎn)擊文字也能觸發(fā) checkbox, 使用label 標(biāo)簽包裹元素, 并觸發(fā) change 事件, 改變 checkbox 的狀態(tài)
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;
// 當(dāng)前是否被選中
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;
}
}
}
實(shí)現(xiàn)用例2: 不可點(diǎn)擊狀態(tài)
不可點(diǎn)擊狀態(tài)和 Button 類似, 加一個不可點(diǎn)擊狀態(tài)的樣式, 不能觸發(fā) checkbox 的 change 事件
- 給父元素添加
disabled className
設(shè)置樣式 -
change
事件disabled
不可觸發(fā) - 添加
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;
// 如何實(shí)現(xiàn)
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;
// 當(dāng)前是否被選中
const [currentChecked, setCurrentChecked] = useState(checked);
const classes_inner = {
"g-checkbox-checked": currentChecked,
};
const handleChange = () => {
// 如果 disabeld 不能觸發(fā) 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;
}
}
}
實(shí)現(xiàn)用例3: change事件回調(diào)
- 我們需要知道 checkbox 的狀態(tài), 傳入 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;
// 實(shí)現(xiàn)
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;
// 當(dāng)前是否被選中
const [currentChecked, setCurrentChecked] = useState(checked);
const classes_inner = {
"g-checkbox-checked": currentChecked,
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// 如果 disabeld 不能觸發(fā) change 事件
if (disabled) return;
setCurrentChecked(!currentChecked);
// 回調(diào)
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;
- 有時候不知道這個元素的類型, 可以把鼠標(biāo)放在元素上, 看一下什么類型
實(shí)現(xiàn) CheckboxGroup 組件
平常我們使用多選框大部分是把多個選擇框放在一起, 成為一組, 選擇多個, CheckboxGroup 組件就是對 Checkbox 組件包裝一下, 當(dāng)觸發(fā) chnage 事件時, 知道我們選擇了哪些選擇框, 而不用給每一個 Checkbox 添加一個 change事件
用例圖 & 使用方式
- 用戶默認(rèn)選中了哪幾個
- 用戶改變選中, 返回選中值
下面我們看一下如何讓用戶使用我們的組件, 第一種方案是 讓用戶寫入每一個 Checkbox 組件, 第二種方案是讓用戶通過數(shù)據(jù)的方式我們自己來渲染這些Checkbox, 這次我先嘗試使用 第一種方式來實(shí)現(xiàn) CheckboxGroup
// 1. 元素方式
<CheckboxGroup selected={["11", "33"]} onChange={handleChange}>
<Checkbox value="11">蘋果</Checkbox>
<Checkbox value="22">香蕉</Checkbox>
<Checkbox value="33" disabled>火龍果</Checkbox>
</CheckboxGroup>
// 2. 數(shù)據(jù)方式
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
不透明數(shù)據(jù)結(jié)構(gòu)的實(shí)用方法
// 如何使用最基本的
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("復(fù)選框組的子元素必須是 Checkbox");
}
// 返回每一個子元素并帶有props
return React.cloneElement(child, {
...child.props,
key: index,
});
});
return <div>{childWithProps}</div>;
};
export default CheckboxGroup;
- 可以看到下面的我們可以正確渲染了
多選框組
, 但是我們不知道我們選中了哪一個, 如果有默認(rèn)選中的我們也不知道怎么設(shè)置, 下面添加兩個屬性-
selected props
用戶初始是否有默認(rèn)選中的值 -
onChange
當(dāng)用戶觸發(fā)子元素事件, 通知父元素選中一組中的哪幾個
-
實(shí)現(xiàn)1: selected 數(shù)組
在父元素
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("復(fù)選框組的子元素必須是 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;
// 當(dāng)前是否被選中
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 不能觸發(fā) 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;
實(shí)現(xiàn)2: change事件
上面我們實(shí)現(xiàn)了 selected, 現(xiàn)在當(dāng)我們改變選中數(shù)組時告訴父元素哪幾個兄弟被選中了, 所以我們需要根據(jù) selected 的值來為初始值, 當(dāng)改變子元素的 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("復(fù)選框組的子元素必須是 Checkbox");
}
return React.cloneElement(child, {
...child.props,
key: index,
selected,
onChange: handleGroupChange, // 利用回調(diào)
});
});
return <div>{childWithProps}</div>;
};
export default CheckboxGroup;
上面完善了 Button組件和Checkbox組件的基本使用, 可以在后面我們寫Table 組件的時候直接使用, 如果需要其它功能, 自己添加一下 props