Neodrag v2 to v3

Neodrag v2 to v3

Migrating from Neodrag v2 to v3

This guide covers migrating from Neodrag v2’s options-based API to v3’s plugin-based architecture.

Why Migrate to v3?

v2 Limitations:

  • Monolithic bundle - pay for unused features
  • Performance issues - 3 event listeners per draggable
  • Limited extensibility - can’t add custom behavior
  • Manual reactivity management

v3 Improvements:

  • Tree-shakable plugins - 50% smaller bundle
  • Event delegation - 3 listeners total regardless of draggable count
  • Custom plugin support
  • Better performance and pointer capture

Architecture Overview

Aspectv2v3
ConfigurationSingle options objectPlugin array
ReactivityAutomaticManual compartments
EventsBuilt-in callbacksEvent plugin
BoundsString or objectBoundsFrom helpers
Bundle Size~2.2KB all features~1.1KB plus only used plugins

Installation

npm install @neodrag/vanilla@next
// Old imports
import { draggable } from '@neodrag/vanilla';

// New imports
import {
  Draggable,
  axis,
  bounds,
  BoundsFrom,
  events,
} from '@neodrag/vanilla';

Basic Usage Examples

Before - v2 Options:

import { draggable } from '@neodrag/vanilla';

const element = document.getElementById('drag-me');
const { destroy } = draggable(element, {
  axis: 'x',
  bounds: 'parent',
  grid: [10, 10],
  onDrag: (data) => console.log(data),
});

After - v3 Plugins:

import {
  Draggable,
  axis,
  bounds,
  BoundsFrom,
  grid,
  events,
} from '@neodrag/vanilla';

const element = document.getElementById('drag-me');
const instance = new Draggable(element, [
  axis('x'),
  bounds(BoundsFrom.parent()),
  grid([10, 10]),
  events({ onDrag: (data) => console.log(data) }),
]);

Plugin Conversion Reference

Movement Control

v2 Optionv3 PluginPurpose
axis: 'x'axis('x')Horizontal only
axis: 'y'axis('y')Vertical only
axis: 'both'no plugin neededBoth directions (default)
disabled: truedisabled()Disable dragging

Bounds and Constraints

v2 Optionv3 PluginPurpose
bounds: 'parent'bounds(BoundsFrom.parent())Constrain to parent element
bounds: '#container'bounds(BoundsFrom.selector('#container'))Constrain to specific element
bounds: elementbounds(BoundsFrom.element(element))Constrain to DOM element
bounds: { top: 50 }bounds(BoundsFrom.viewport({ top: 50 }))Constrain to viewport with padding

Grid and Snapping

v2 Optionv3 PluginPurpose
grid: [20, 20]grid([20, 20])Snap to grid
threshold: { distance: 5 }threshold({ distance: 5 })Movement threshold
threshold: { delay: 100 }threshold({ delay: 100 })Time delay threshold

Controls and Interaction

v2 Optionv3 PluginPurpose
handle: '.handle'controls({ allow: ControlFrom.selector('.handle') })Drag only from handle
cancel: '.cancel'controls({ block: ControlFrom.selector('.cancel') })Prevent drag from area
ignoreMultitouch: trueignoreMultitouch(true)Ignore multiple touches

Events and Callbacks

Events moved from options to dedicated plugin. Event data structure changed:

  • v2: offsetX, offsetY, rootNode, currentNode, event
  • v3: offset object with x and y properties, rootNode, currentNode, event

v2 Events:

draggable(element, {
  onDragStart: ({ offsetX, offsetY }) =>
    console.log('start', offsetX, offsetY),
  onDrag: ({ offsetX, offsetY }) =>
    console.log('drag', offsetX, offsetY),
  onDragEnd: ({ offsetX, offsetY }) =>
    console.log('end', offsetX, offsetY),
});

v3 Events:

new Draggable(element, [
  events({
    onDragStart: ({ offset }) =>
      console.log('start', offset.x, offset.y),
    onDrag: ({ offset }) => console.log('drag', offset.x, offset.y),
    onDragEnd: ({ offset }) => console.log('end', offset.x, offset.y),
  }),
]);

Reactive Updates: The Biggest Change

v2 automatically updated when options changed. v3 requires manual compartments for reactive behavior.

v2 Manual Updates:

let currentAxis = 'x';
let isConstrained = false;
let instance = draggable(element, {
  axis: currentAxis,
  bounds: isConstrained ? 'parent' : undefined,
});

function updateSettings() {
  instance.destroy();
  instance = draggable(element, {
    axis: currentAxis,
    bounds: isConstrained ? 'parent' : undefined,
  });
}

document.getElementById('axis-btn').addEventListener('click', () => {
  currentAxis = 'y';
  updateSettings();
});

v3 Easy Compartments:

import { Compartment } from '@neodrag/vanilla';

let currentAxis = 'x';
let isConstrained = false;

const axisComp = new Compartment(() =>
  currentAxis === 'both' ? null : axis(currentAxis),
);
const boundsComp = new Compartment(() =>
  isConstrained ? bounds(BoundsFrom.parent()) : null,
);

const instance = new Draggable(element, () =>
  [axisComp, boundsComp].filter(Boolean),
);

document.getElementById('axis-btn').addEventListener('click', () => {
  currentAxis = 'y';
  axisComp.current = axis(currentAxis);
});

document
  .getElementById('bounds-btn')
  .addEventListener('click', () => {
    isConstrained = !isConstrained;
    boundsComp.current = isConstrained
      ? bounds(BoundsFrom.parent())
      : null;
  });

New v3 Features

Custom Plugins

Create your own dragging behavior:

import { Draggable, unstable_definePlugin } from '@neodrag/vanilla';

const loggerPlugin = unstable_definePlugin(() => ({
  name: 'logger',
  setup(ctx) {
    console.log('Drag initialized');
    return { startTime: 0 };
  },
  start(ctx, state, event) {
    state.startTime = Date.now();
    console.log('Drag started');
  },
  drag(ctx, state, event) {
    const duration = Date.now() - state.startTime;
    console.log(`Dragging for ${duration}ms`);
  },
  end(ctx, state, event) {
    console.log('Drag ended');
  },
}));

const element = document.getElementById('custom');
const instance = new Draggable(element, [loggerPlugin]);

ScrollLock Plugin

Prevent page scrolling during drag:

import { Draggable, scrollLock } from '@neodrag/vanilla';

const element = document.getElementById('scroll-lock');
const instance = new Draggable(element, [
  scrollLock({
    lockAxis: 'both',
    allowScrollbar: false,
  }),
]);

Framework-Specific Syntax Changes

Key Vanilla Changes:

  • Function call → Class instantiation: draggable(el, opts)new Draggable(el, plugins)
  • Add Compartment class for reactive updates
  • Instance methods remain similar: instance.destroy()

Migration Checklist

Step 1: Update Dependencies

  • Install v3 package for your framework
  • Update import statements to include needed plugins
  • Update TypeScript types if using TypeScript

Step 2: Convert Options to Plugins

  • Replace options object with plugin array
  • Convert each option using the reference tables above
  • Import required plugin functions

Step 3: Handle Reactive Updates

  • Identify dynamic/reactive options in v2 code
  • Create compartments for each reactive value
  • Wire up compartment updates to state changes

Step 4: Update Framework Syntax

  • Apply framework-specific syntax changes
  • Update event handlers to use events plugin
  • Test basic functionality

Step 5: Update Event Data Usage

  • Change from offsetX/offsetY to offset.x/offset.y
  • Update any event handler logic
  • Verify all event callbacks work correctly

Step 6: Test and Validate

  • Test all drag behaviors
  • Verify reactive updates work with compartments
  • Check performance and bundle size improvements
  • Test on different devices and browsers

Common Migration Issues

Missing Plugin Imports Make sure to import all plugins you use. Forgetting imports is the most common issue.

Reactivity Not Working Remember to use compartments for any value that should update dynamically. v3 doesn’t have automatic reactivity like v2.

Events Not Firing Events are now handled by the events plugin, not built-in options. Add the events plugin with your callbacks.

TypeScript Errors Update type imports and ensure you’re using the correct v3 types for your framework.

Performance Benefits

Memory Usage: Event delegation means 3 total listeners instead of 3 per draggable
Reliability: Pointer capture prevents losing drag when moving over iframes
Extensibility: Custom plugins allow community-driven extensions

The migration requires some refactoring, but v3 provides significant improvements in performance, bundle size, and flexibility. Take it step-by-step, use compartments for reactivity, and enjoy the enhanced capabilities of Neodrag v3!