Skip to content

Feature Request: Scoped Interactable instances for modular & granular management #1068

@NotYoojun

Description

@NotYoojun

Summary

Add optional granular interaction management to interact.js while maintaining full backward compatibility. This allows multiple components to independently manage their interactions on the same element without conflicts.

Problem Description

Currently, all interact() calls for the same element return the same global instance, causing conflicts when multiple components try to manage different interaction types independently:

// Component A
const interactableA = interact(element).draggable({ /* config */ });

// Component B  
const interactableB = interact(element).gesturable({ /* config */ });

// Problem: interactableA === interactableB (same global instance!)
// When Component A cleans up:
interactableA.unset(); // This removes EVERYTHING, breaking Component B

Proposed Solution

Add an optional granular flag to create unique scoped instances:

New Granular API

// Create unique scoped instances
const scopedA = interact(element, { granular: true });
const scopedB = interact(element, { granular: true });

console.log(scopedA === scopedB); // false - different instances!

// Configure independently
scopedA.draggable({ /* config A */ });
scopedB.gesturable({ /* config B */ });

// Clean up independently
scopedA.unset(); // Only removes draggable, gesturable remains intact
scopedB.unset(); // Only removes gesturable, draggable remains intact

Enhanced Global Cleanup

// Static method for complete cleanup (removes ALL interactions)
interact.unset(element); // Removes both global and all granular instances

// Instance method for backward compatibility
interact(element).unsetAll(); // Same as above

Backward Compatibility

// Existing API remains unchanged - no breaking changes!
const globalInstance = interact(element);
const sameGlobal = interact(element);

console.log(globalInstance === sameGlobal); // true - same behavior as before
globalInstance.unset(); // Works exactly as it does today

Implementation Example

// Our real-world use case - this would solve our architectural problem perfectly:

class TheScene2DController {
    private canvasInteractable: Interactable | null = null;

    private addCanvasEvents(canvas: HTMLCanvasElement) {
        canvas.addEventListener("pointerdown", this.canvas_PointerDown);
        canvas.addEventListener("wheel", this.canvas_PointerWheel);

        // Create a granular instance for this component
        this.canvasInteractable = interact(canvas, { granular: true });
        this.canvasInteractable.gesturable({
            onstart: (e) => this.canvas_GestureStart(canvas, e),
            onmove: (e) => this.canvas_GestureMove(canvas, e),
            onend: (e) => this.canvas_GestureEnd(canvas, e),
        });
    }

    private removeCanvasEvents(canvas: HTMLCanvasElement) {
        canvas.removeEventListener("pointerdown", this.canvas_PointerDown);
        canvas.removeEventListener("wheel", this.canvas_PointerWheel);

        // PERFECT: Only removes THIS component's interactions
        this.canvasInteractable?.unset();
        this.canvasInteractable = null;
        
        // Other components' interactions remain completely untouched!
    }
}

With this approach, each component can create, manage and clean its own interactions without interfering with others, while still allowing for a complete cleanup when needed.

// Before (problematic)
const interactable = interact(element);

// After (granular)
const interactable = interact(element, { granular: true });

Real-World Use Cases

This solves architectural problems in:

  • Canvas editors (like our project)
  • Component libraries (Material-UI, Ant Design, etc.)
  • Multi-widget dashboards
  • Games with layered interactions
  • Any modular web application

Why This Approach?

  1. Minimal API Surface: Only one new optional parameter
  2. Clear Intent: granular: true explicitly indicates scoped behavior
  3. No Coordination Required: Components don't need to know about each other
  4. Performance Friendly: No overhead for existing users
  5. Future Proof: Extensible for additional scoping features

This solution provides the architectural flexibility modern applications need while preserving the simplicity that makes interact.js popular.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions