react实现穿梭框

291 阅读3分钟

穿梭框

在一些常见的业务开发中,我们经常会使用到穿梭框,在一些常见的ui中,如antd,semi等,都有一定的实现,本文基于react,探索一下穿梭框的实现

实现分析

  1. 以antd为例, 可以看到组件有三个部分,左右两个List容器,中间Button容器,以及选项item组件

image.png

image.png 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>

);

};

组件实现

  1. 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来获取操作数据的方法,分别绑定到对应的结构上

  1. Btn组件, 主要是控制穿梭按钮,具体方法定义在了Provider中,通过context拿到方法
const Btn = () => {

    const { toLeft, toRight } = useTransfer();

    return (
        <div className={styles["btn-group"]}>
            <button onClick={toLeft}>&lt;</button>
            <button onClick={toRight}>&gt;</button>
        </div>
    );
};

  1. TransferContent组件
const TransferContent = () => {

    const { leftD, rightD } = useTransfer();

    return (
        <div className={styles.container}>
            <List data={leftD} dir="left" />
            <Btn />
            <List data={rightD} dir="right" />
        </div>
    );
};
  1. 使用Provider包裹
export const Transfer = () => {

    return (
        <Provider>
            <TransferContent />
        </Provider>
    );
};

实现效果

image.png

以上就是本人基于react对穿梭框的实现,如果各位大佬有更好的实现方式,请在评论区多多指教