-
Notifications
You must be signed in to change notification settings - Fork 798
Description
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?
- Minimal API Surface: Only one new optional parameter
- Clear Intent:
granular: true
explicitly indicates scoped behavior - No Coordination Required: Components don't need to know about each other
- Performance Friendly: No overhead for existing users
- 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.