穿梭框
在一些常见的业务开发中,我们经常会使用到穿梭框,在一些常见的ui中,如antd,semi等,都有一定的实现,本文基于react,探索一下穿梭框的实现
实现分析
- 以antd为例, 可以看到组件有三个部分,左右两个List容器,中间Button容器,以及选项item组件
2. 数据定义
我们可以定义每一个item的数据结构如下
interface IItem {
id: number; // id
text: string; // 内容
completed: boolean; // 是否选中
}
根据数据结构,可以实现一些测试数据
const left: IItem[] = [
{
id: 1,
text: "demo1",
completed: false,
},
{
id: 2,
text: "demo2",
completed: false,
},
{
id: 3,
text: "demo3",
completed: false,
},
];
数据传递
考虑到数据在整个组件树内流转,操作数据的方法在不同的组件上也需要不一样的实现,因此可以将数据实现为一个Context, 通过Provider传递到全局组件上,可以共享到数据的方法,避免多层级的props传递,便于统一管理
type ContextType = {
leftD: IItem[]; // 左边List数据
toLeft: () => void; // 将item从右边传递到左边
rightD: IItem[]; // 右边数据
toRight: () => void; // 将item从左边传递到右边
handleSelected: (status: boolean, id: number, dir: "left" | "right") => void; // item上的 onChange事件,可以操作leftD / rightD数据
leftAll: boolean; // 左边全选
rightAll: boolean; // 右边全选
selectAll: (status: boolean, dir: "left" | "right") => void; // 全选按钮方法
reverseSelect: (dir: "left" | "right") => void; // 反选按钮方法
};
const Context = createContext<ContextType | undefined>(undefined);
// 定义Provider组件,传递全局数据
const Provider = ({ children }: { children: React.ReactNode }) => {
const [leftD, setLeftD] = useState<IItem[]>(() => left);
const [rightD, setRightD] = useState<IItem[]>(() => []);
// 全选数据根据左右两边各自的data情况判断,无需在定义state
const leftAll = useMemo(
() => leftD.length > 0 && leftD.every((item) => item.completed),
[leftD]
);
const rightAll = useMemo(
() => rightD.length > 0 && rightD.every((item) => item.completed),
[rightD]
);
const handleSelected = (status: boolean, id: number, dir: "left" | "right") => {
if (dir === "left") {
setLeftD((prev) => {
return prev.map((item) => {
if (item.id === id) {
item.completed = status;
}
return item;
});
});
} else {
setRightD((prev) => {
return prev.map((item) => {
if (item.id === id) {
item.completed = status;
}
return item;
});
});
}
};
// toRight
const toRight = () => {
setRightD((right) => {
const selected = leftD.filter((item) => item.completed);
return [...right, ...selected];
});
setLeftD((left) => {
return left.filter((item) => !item.completed);
});
};
// toLeft
const toLeft = () => {
setLeftD((left) => {
const selected = rightD.filter((item) => item.completed);
return [...left, ...selected];
});
setRightD((right) => {
return right.filter((item) => !item.completed);
});
};
// 全选
const selectAll = (status: boolean, dir: "left" | "right") => {
if (dir === "left") {
setLeftD((prev) => {
return prev.map((item) => {
item.completed = status;
return item;
});
});
} else {
setRightD((prev) => {
return prev.map((item) => {
item.completed = status;
return item;
});
});
}
};
const reverseSelect = (dir: "left" | "right") => {
if (dir === "left") {
setLeftD((prev) => {
return prev.map((item) => {
item.completed = !item.completed;
return item;
});
});
} else {
setRightD((prev) => {
return prev.map((item) => {
item.completed = !item.completed;
return item;
});
});
}
};
return (
<Context.Provider
value={{
leftD,
toLeft,
rightD,
toRight,
handleSelected,
leftAll,
rightAll,
selectAll,
reverseSelect,
}}
>
{children}
</Context.Provider>
);
};
组件实现
- List组件
const List = ({ data, dir }: { data: IItem[]; dir: "left" | "right" }) => {
const { leftAll, rightAll, selectAll, reverseSelect } = useTransfer();
return (
<div className={styles.list}>
<div className={styles.selectAll}>
<label>
全选
<input
type="checkbox"
checked={dir === "left" ? leftAll : rightAll}
onChange={(e) => selectAll(e.target.checked, dir)}
/>
</label>
<label>
反选
<input type="checkbox" onChange={() => reverseSelect(dir)} />
</label>
</div>
<div>
{data.map((item) => {
return <Item key={item.id} dir={dir} item={item} />;
})}
</div>
</div>
);
};
在List组件中, data是要组件要渲染的数据,dir主要用来区分穿梭框左右两边,方便进行数据操作;在组件内部,使用定义的context hook来获取操作数据的方法,分别绑定到对应的结构上
- Btn组件, 主要是控制穿梭按钮,具体方法定义在了Provider中,通过context拿到方法
const Btn = () => {
const { toLeft, toRight } = useTransfer();
return (
<div className={styles["btn-group"]}>
<button onClick={toLeft}><</button>
<button onClick={toRight}>></button>
</div>
);
};
- TransferContent组件
const TransferContent = () => {
const { leftD, rightD } = useTransfer();
return (
<div className={styles.container}>
<List data={leftD} dir="left" />
<Btn />
<List data={rightD} dir="right" />
</div>
);
};
- 使用Provider包裹
export const Transfer = () => {
return (
<Provider>
<TransferContent />
</Provider>
);
};
实现效果
以上就是本人基于react对穿梭框的实现,如果各位大佬有更好的实现方式,请在评论区多多指教