TahaSh / swapy

✨ A framework-agnostic tool that converts any layout into a drag-to-swap one with just a few lines of code https://swapy.tahazsh.com/
MIT License
6.67k stars 136 forks source link

Could swapy be implemented to support react-native? #89

Open Veeksi opened 3 weeks ago

Veeksi commented 3 weeks ago

The title says it and two words, amazing library!

TahaSh commented 1 week ago

Yes it works with React Native. Let me know if you're having any issue with that.

Veeksi commented 1 week ago

Yes it works with React Native. Let me know if you're having any issue with that.

Hmm do you have an example with React Native? I am getting this warning from the library validate function when trying to run it on react-native: Warning: TypeError: r.querySelectorAll is not a function (it is undefined)

I feel like the problem is that react-native doesn't allow to directly access DOM elements like in web development.

My implementation looks like this:

import { useEffect, useMemo, useRef, useState } from "react";
import { Button, StyleSheet, Text, View, findNodeHandle } from "react-native";
import { type SlotItemMap, type Swapy, createSwapy } from "swapy";

const SwapyComponent = () => {
    const swapyRef = useRef<Swapy>();
    const containerRef = useRef(null);

    const [items, setItems] = useState([
        { id: "1", title: "A" },
        { id: "2", title: "B" },
        { id: "3", title: "C" },
    ]);

    const [slotItemsMap, setSlotItemsMap] = useState<SlotItemMap>([
        ...items.map((item) => ({
            slotId: item.id,
            itemId: item.id,
        })),
        // Defining an empty slot by setting itemId to null.
        { slotId: `${Math.round(Math.random() * 99999)}`, itemId: null },
    ]);

    const slottedItems = useMemo(
        () =>
            slotItemsMap.map(({ slotId, itemId }) => ({
                slotId,
                itemId,
                item: items.find((item) => item.id === itemId),
            })),
        [items, slotItemsMap],
    );

    // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
    useEffect(() => {
        // Get the newly added items and convert them to slotItem objects
        const newItems = items
            .filter(
                (item) => !slotItemsMap.some((slotItem) => slotItem.itemId === item.id),
            )
            .map((item) => ({
                slotId: item.id,
                itemId: item.id,
            }));

        // Remove items from slotItemsMap if they no longer exist in items
        const withoutRemovedItems = slotItemsMap.filter(
            (slotItem) =>
                items.some((item) => item.id === slotItem.itemId) || !slotItem.itemId,
        );

        /******* Below is how you would remove items and keep their slots empty ******/
        // const withoutRemovedItems = slotItemsMap.map(slotItem => {
        //   if (!items.some(item => item.id === slotItem.itemId)) {
        //     return { slotId: slotItem.slotId, itemId: null }
        //   }
        //   return slotItem
        // })

        const updatedSlotItemsMap = [...withoutRemovedItems, ...newItems];

        setSlotItemsMap(updatedSlotItemsMap);
        swapyRef.current?.setData({ array: updatedSlotItemsMap });
    }, [items]);

    useEffect(() => {
        const containerNode = findNodeHandle(containerRef.current);
        swapyRef.current = createSwapy(containerNode, {
            manualSwap: true,
            swapMode: "hover",
            autoScrollOnDrag: true,
        });

        swapyRef.current.onSwap(({ data }) => {
            // You need to call setData because it's a manualSwap instance
            swapyRef.current?.setData({ array: data.array });
            setSlotItemsMap(data.array);
        });

        return () => {
            swapyRef.current?.destroy();
        };
    }, []);

    return (
        <View style={styles.app}>
            {/* ADD BUTTON */}
            <Button
                title="Add"
                onPress={() => {
                    const id = `${Math.round(Math.random() * 99999)}`;
                    const updatedItems = [...items, { id, title: id }];
                    setItems(updatedItems);
                }}
            />

            <View ref={containerRef} style={styles.container}>
                {slottedItems.map(({ itemId, slotId, item }) => (
                    <View style={styles.slot} key={slotId}>
                        {/* ITEM */}
                        {item ? (
                            <View style={styles.item} key={itemId}>
                                <View style={styles.handle} />
                                <Text>{item.title}</Text>

                                {/* DELETE ITEM BUTTON */}
                                <Button
                                    title="x"
                                    onPress={() => {
                                        const updatedItems = items.filter((i) => i.id !== item.id);
                                        setItems(updatedItems);
                                    }}
                                />
                            </View>
                        ) : null}
                    </View>
                ))}
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    app: {
        flex: 1,
        padding: 20,
    },
    container: {
        flex: 1,
        marginTop: 20,
    },
    slot: {
        marginBottom: 10,
        padding: 10,
        borderWidth: 1,
        borderColor: "#ccc",
    },
    item: {
        padding: 10,
        backgroundColor: "#f9f9f9",
    },
    handle: {
        height: 10,
        backgroundColor: "#ccc",
        marginBottom: 5,
    },
});

export default SwapyComponent;