Интеграция с состоянием приложения

Dragula — это легковесная библиотека для реализации перетаскивания элементов (drag-and-drop) в веб-приложениях на JavaScript. Она ориентирована на простоту и минимальные зависимости, обеспечивая удобный API для перемещения элементов между контейнерами. Основные возможности включают: перетаскивание внутри одного контейнера, между несколькими контейнерами, возможность отмены перемещения и события жизненного цикла drag-and-drop.

Для работы Dragula достаточно подключить скрипт и стили, после чего создать экземпляр функции dragula с массивом контейнеров. Пример базового подключения:

const containers = [document.getElementById('left'), document.getElementById('right')];
const drake = dragula(containers);

Эта конструкция сразу позволяет перемещать элементы между контейнерами без дополнительной конфигурации.


Настройка ограничений перемещения

Dragula предоставляет возможность настраивать, какие элементы можно перемещать, а какие нет, через функцию moves. Она вызывается при начале перетаскивания и получает три параметра: элемент, родительский контейнер и событие мыши. Возврат true разрешает перемещение, false запрещает.

const drake = dragula(containers, {
    moves: function (el, source, handle, sibling) {
        return !el.classList.contains('fixed');
    }
});

В этом примере элементы с классом fixed невозможно перетащить.

Другой важный параметр — accepts, который определяет, можно ли помещать перетаскиваемый элемент в конкретный контейнер:

const drake = dragula(containers, {
    accepts: function(el, target, source, sibling) {
        return target.id !== 'left'; // запрещаем помещать элементы в левый контейнер
    }
});

События жизненного цикла

Dragula генерирует набор событий, позволяющих интегрироваться с состоянием приложения. Основные из них:

  • drag — начинается перетаскивание элемента.
  • dragend — завершение перетаскивания, независимо от того, было ли изменение позиции.
  • drop — элемент был перемещён в новый контейнер.
  • remove — элемент был удалён.
  • cancel — перетаскивание отменено.

Подписка на событие осуществляется через метод .on:

drake.on('drop', function(el, target, source, sibling) {
    console.log(`Элемент ${el.id} перемещён из ${source.id} в ${target.id}`);
});

События drop и remove особенно важны для синхронизации состояния приложения с UI.


Интеграция с состоянием приложения

Чтобы обеспечить синхронизацию интерфейса с внутренними данными, необходимо отражать изменения Dragula в состоянии приложения. В современных приложениях на React, Vue или Svelte это часто означает обновление массива объектов, соответствующего DOM-структуре.

Пример для чистого JavaScript с объектом состояния:

const state = {
    left: ['item1', 'item2'],
    right: ['item3', 'item4']
};

drake.on('drop', function(el, target, source, sibling) {
    const sourceArray = state[source.id];
    const targetArray = state[target.id];

    const index = sourceArray.indexOf(el.id);
    if (index !== -1) sourceArray.splice(index, 1);

    const siblingIndex = sibling ? targetArray.indexOf(sibling.id) : targetArray.length;
    targetArray.splice(siblingIndex, 0, el.id);
});

В этом примере перемещение DOM-элемента автоматически отражается в массиве состояния, что обеспечивает корректную дальнейшую работу с данными, например, для отправки на сервер или сохранения в localStorage.


Dragula и реактивные фреймворки

React: В React использование Dragula требует работы с refs и синхронизации состояния. Контейнеры передаются через ref, а события drop используются для обновления состояния через setState или useState.

const leftRef = useRef(null);
const rightRef = useRef(null);

useEffect(() => {
    const drake = dragula([leftRef.current, rightRef.current]);
    drake.on('drop', (el, target, source, sibling) => {
        setState(prev => {
            const newState = {...prev};
            // аналогично обновляем массивы элементов
            return newState;
        });
    });
}, []);

Vue: В Vue Dragula интегрируется через ref и методы жизненного цикла mounted. Изменение массива данных Vue автоматически обновляет DOM, но необходимо обеспечить обратное обновление состояния при перемещении элементов.

mounted() {
    const drake = dragula([this.$refs.left, this.$refs.right]);
    drake.on('drop', (el, target, source, sibling) => {
        // обновляем реактивные массивы
    });
}

Svelte: В Svelte использование Dragula требует прямой работы с DOM через bind:this и реактивных переменных. Перетаскивание элементов должно сопровождаться изменением массивов, чтобы сохранить реактивность.


Работа с асинхронными операциями

При интеграции с состоянием приложения часто возникает необходимость выполнять асинхронные операции, например, запрос на сервер после перемещения элемента. В таких случаях событие drop может вызывать функцию, которая сначала обновляет локальное состояние, а затем синхронизирует изменения с сервером:

drake.on('drop', async (el, target, source, sibling) => {
    updateLocalState(el, target, source, sibling);
    try {
        await sendUpdateToServer(state);
    } catch (error) {
        revertLocalState();
        console.error('Ошибка при сохранении на сервере', error);
    }
});

Такой подход позволяет обеспечить реактивность интерфейса без блокировки пользователя, одновременно сохраняя целостность данных.


Стилизация элементов при перетаскивании

Dragula автоматически добавляет класс gu-transit перетаскиваемому элементу и gu-mirror — его копии, следящей за курсором. Эти классы можно использовать для кастомной стилизации:

.gu-mirror {
    opacity: 0.8;
    border: 1px dashed #aaa;
}

.gu-transit {
    background-color: #f0f0f0;
}

Дополнительно, события drag и dragend позволяют динамически менять стили элементов или контейнеров в процессе перемещения.


Использование Dragula с множественными контейнерами

Dragula поддерживает перемещение элементов между произвольным числом контейнеров. При этом важно корректно настраивать accepts и moves, чтобы предотвратить нежелательные перемещения. Для динамически добавляемых контейнеров можно использовать метод drake.containers.push(newContainer), что позволит библиотеке учитывать новый контейнер без перезагрузки.


Отмена и возврат перемещений

Для случаев, когда перемещение нужно отменить, Dragula предоставляет метод drake.cancel(el) и событие cancel. Его можно использовать для реализации логики undo:

drake.on('cancel', function(el, container, source) {
    console.log(`Перемещение элемента ${el.id} отменено`);
});

Это особенно важно при интеграции с состоянием приложения, чтобы предотвратить рассинхронизацию DOM и данных.


Итоговая архитектура интеграции

Корректная интеграция Dragula в приложение строится по принципу двусторонней синхронизации:

  1. DOM → Состояние: изменения через события Dragula (drop, remove) обновляют внутренние массивы данных.
  2. Состояние → DOM: изменения в состоянии автоматически отражаются на DOM при перерендере.
  3. Асинхронная синхронизация: обновления состояния сопровождаются серверными запросами с обработкой ошибок и возможной отменой изменений.
  4. Кастомизация стилей и поведения: классы Dragula и функции moves/accepts обеспечивают визуальную и логическую настройку перетаскивания.

Такой подход позволяет строить сложные интерфейсы с множественными контейнерами и гарантирует стабильность состояния при любых действиях пользователя.