05_drag_and_drop_system_.md - ganeshramakrishnanbr/Jumbo_NoCodeBuilder GitHub Wiki
Welcome back! We've come a long way. We now understand the basic building blocks: the different ControlTypes (like "Text Box" or "Tab"), the individual Control instances that represent each piece on your form, how the entire design is held together in a single Questionnaire object, and how the QuestionnaireContext acts as the central brain managing all this data.
But how do you actually put those building blocks together visually? How do you move that Text Box onto the canvas? How do you rearrange controls after you've added them?
This is where the Drag and Drop System comes in!
Think about building with physical blocks. You pick up a block, carry it, and place it where you want it to go. The Drag and Drop System in our builder is the digital equivalent of that. It's the tool that lets you use your mouse to:
- Pick a new Control type from the "Control Palette" (the list of available controls, usually on the side).
- Drag it over to the "Design Canvas" (the main area where you build your form).
- Drop it onto the canvas or inside another container control (like a Tab or Accordion section).
- Pick up an existing Control on the Design Canvas and move it to a different position or into a different container.
While you're dragging, the system handles showing visual cues (like highlights) to indicate where you can drop the item and where it will end up. When you drop, the system figures out what happened and tells the QuestionnaireContext to update the underlying Questionnaire data accordingly (either by adding a new Control or moving an existing one).
Let's use our running example: You want to add a Text Box to your form. You see the "Text Box" item in the Control Palette and you want to drag it onto the empty canvas.
This visual action needs to translate into a change in the Questionnaire data managed by the QuestionnaireContext. Specifically, the system needs to create a new Control object with type: ControlType.TextBox
and add it to the controls
array within the Questionnaire
object. The Drag and Drop system is the bridge that makes this happen based on your mouse actions.
The Drag and Drop system involves several parts working together:
-
Drag Source: This is the element you click and start dragging. It could be an item in the Control Palette (representing a new control) or an existing Control instance on the Design Canvas. The source needs to carry information about what is being dragged (e.g., its
ControlType
if new, or itsid
if existing). - Drop Target: This is the area where you can release a dragged item. The Design Canvas is a main drop target. Container controls (like Tabs or Accordions) also have areas that act as drop targets for dropping controls inside them. Drop targets need to be able to validate if the currently dragged item is allowed to be dropped there (e.g., can you drop a Tab inside a Text Box? Probably not).
- Drag State: While an item is being dragged, the system keeps track of information about it – what it is, where the drag started, whether it's a new item or an existing one. This state is often managed globally so both the source and potential targets can access it.
- Visual Cues: As you drag an item over a drop target, the system provides visual feedback. This could be highlighting the target area, showing an insertion line to indicate where the item will be placed, or changing the cursor.
- Drop Logic: When you release the mouse button over a valid drop target, the drop target's code runs. It reads the information about the dragged item (from the drag state) and decides what action to take. This action then typically involves calling functions on the QuestionnaireContext to modify the actual form data.
Let's trace the path of adding a new Text Box from the palette to the canvas using Drag and Drop and how it interacts with the QuestionnaireContext:
sequenceDiagram
Participant U as User
Participant CP as ControlPalette
Participant DDC as DragDropContext
Participant DC as DesignCanvas
Participant QC as QuestionnaireContext
U->>CP: Clicks and drags "Text Box"
CP->>DDC: Calls startDrag({ controlType: ControlType.TextBox, isNew: true, ... })
Note over DDC: DDC stores info about the dragged Text Box (it's new)
U->>DC: Drags over the Design Canvas area
DC->>DDC: Calls canDropIn('canvas')?
DDC-->>DC: Returns true (Canvas is a valid target for new controls)
DC->>DC: Shows visual drop indicator (e.g., blue border)
U->>DC: Releases mouse (Drops)
DC->>DC: Handle drop event
DC->>DDC: Gets draggedItem info (ControlType.TextBox, isNew: true)
DC->>DC: Creates data for a new Control instance based on type
DC->>QC: Calls addControl({ type: ControlType.TextBox, label: 'New Text Box', ... })
Note over QC: QC generates ID, adds Control to Questionnaire.controls array
QC-->>DC: State updates, DC re-renders
DC->>DC: Shows the new Text Box on the canvas
DC->>DDC: Calls endDrag()
Note over DDC: DDC clears dragged item state
This diagram illustrates how the visual action (dragging from Control Palette, dropping on Design Canvas) is managed by the DragDropContext
, and how the DesignCanvas
component uses the information from the DragDropContext
to ultimately call the appropriate function (addControl
) on the QuestionnaireContext
to update the underlying form data.
Let's look at how this is implemented using React and the Context API, specifically focusing on the interaction between ControlPalette
, DragDropContext
, and DesignCanvas
.
First, the ControlPalette
initiates the drag:
// --- File: src/components/designer/controls/ControlPalette.tsx ---
import React from 'react';
import { useDragDrop } from '../../../contexts/DragDropContext'; // Import the DragDrop hook
import { ControlType } from '../../../types';
// ... icon imports ...
const ControlPalette: React.FC = () => {
// Use the hook to get access to drag/drop functions
const { startDrag } = useDragDrop();
// ... control definitions ...
// This function is called when a palette item starts being dragged
const handleDragStart = (controlType: ControlType, label: string) => {
console.log('[ControlPalette] Starting drag for:', controlType);
// Call startDrag from the context, providing info about the item
startDrag({
id: `new-${controlType}-${Date.now()}`, // Temporary ID for the drag operation
type: 'control', // What general type of item is it? (e.g., 'control', could be 'section', etc.)
controlType, // <-- The specific type (e.g., ControlType.TextBox)
isNew: true, // <-- Important flag: this is a new control
// No sourceId or sourceIndex needed as it's new from the palette
});
};
// ... render functions calling handleDragStart on draggable elements ...
return (
<div>
{/* ... groups and controls ... */}
{/* Example item: */}
<div
draggable // This makes the div a drag source
onDragStart={() => handleDragStart(ControlType.TextBox, 'Text Box')} // Call our handler on drag start
className="..."
>
{/* ... icon and label ... */}
</div>
{/* ... other controls ... */}
</div>
);
};
export default ControlPalette;
The ControlPalette
uses the useDragDrop
hook to access the startDrag
function provided by the DragDropContext
. When a user starts dragging one of the palette items (because the div
has the draggable
attribute and an onDragStart
handler), handleDragStart
is called, which in turn calls startDrag
, passing the controlType
and setting isNew
to true
.
Next, the DragDropContext
itself manages the state of the drag operation:
// --- File: src/contexts/DragDropContext.tsx ---
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
import { ControlType } from '../types';
// Interface defining the data for an item being dragged
interface DragItem {
id: string; // Unique ID for this drag action (or control ID if existing)
type: string; // General type of item ('control', 'tab', 'section', etc.)
controlType: ControlType; // Specific control type being dragged
sourceId?: string; // ID of the container where the drag started (if existing)
isNew?: boolean; // Is this a new item from the palette?
sourceIndex?: number; // Index within the source container (if existing)
}
// Interface defining the context value provided to components
interface DragDropContextType {
draggedItem: DragItem | null; // The currently dragged item info
isDragging: boolean; // Simple flag if any drag is active
startDrag: (item: DragItem) => void; // Function to start a drag
endDrag: () => void; // Function to end a drag
canDropIn: (targetType: string) => boolean; // Function to validate drop targets
getSourceContainer: () => string | null; // Helper to get source container
getSourceIndex: () => number | null; // Helper to get source index
}
// Create the context
const DragDropContext = createContext<DragDropContextType | undefined>(undefined);
export const DragDropProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// State to hold info about the dragged item
const [draggedItem, setDraggedItem] = useState<DragItem | null>(null);
const [isDragging, setIsDragging] = useState(false);
// Refs to store transient info about the drag source (more complex handling in actual code)
const sourceContainerRef = useRef<string | null>(null);
const sourceIndexRef = useRef<number | null>(null);
// Function called by drag sources (like ControlPalette or CanvasControl)
const startDrag = (item: DragItem) => {
console.log('[DragDropContext] Starting drag:', item);
setDraggedItem(item); // Store the item being dragged
setIsDragging(true); // Set dragging flag
// Store source info if provided
sourceContainerRef.current = item.sourceId || 'canvas'; // Default to canvas source if none specified
sourceIndexRef.current = item.sourceIndex !== undefined ? item.sourceIndex : null;
};
// Function called by drop targets when drop is complete
const endDrag = () => {
console.log('[DragDropContext] Ending drag.');
setDraggedItem(null); // Clear the dragged item state
setIsDragging(false); // Clear dragging flag
sourceContainerRef.current = null; // Clear source info
sourceIndexRef.current = null;
};
// Function used by potential drop targets to check if the current item can be dropped there
const canDropIn = (targetType: string): boolean => {
if (!draggedItem) return false; // Cannot drop if nothing is dragged
console.log(`[DragDropContext] Checking drop validity: dragged ${draggedItem.controlType} (${draggedItem.isNew ? 'new' : 'existing'}) into ${targetType}`);
// Example validation logic:
// Container controls (Tab, Accordion, ColumnLayout) can only be dropped directly on the canvas
if ([ControlType.Tab, ControlType.Accordion, ControlType.ColumnLayout].includes(draggedItem.controlType)) {
return targetType === 'canvas';
}
// Basic and specialized controls can be dropped on canvas or inside specific containers
return ['tab', 'column', 'accordion', 'canvas'].includes(targetType);
};
// ... getSourceContainer, getSourceIndex helper functions ...
// Provide the state and functions to wrapped components
return (
<DragDropContext.Provider
value={{
draggedItem,
setDraggedItem, // (Sometimes needed for complex scenarios)
isDragging,
startDrag,
endDrag,
canDropIn,
getSourceContainer,
getSourceIndex,
}}
>
{children} {/* The rest of your application */}
</DragDropContext.Provider>
);
};
// Hook for components to easily access the context
export const useDragDrop = () => {
const context = useContext(DragDropContext);
if (context === undefined) {
throw new Error('useDragDrop must be used within a DragDropProvider');
}
return context;
};
The DragDropProvider
wraps part of the application (like the main designer area) and holds the draggedItem
and isDragging
state using useState
. The startDrag
function updates this state when a drag begins, and endDrag
clears it. The canDropIn
function contains the core logic for determining if a drop is allowed based on the draggedItem
type and the type of the potential drop target (targetType
). Any component wanting to initiate a drag or act as a drop target uses the useDragDrop
hook to access these.
Finally, the DesignCanvas
component acts as a major drop target and uses the information from the DragDropContext
to handle drops:
// --- File: src/components/designer/canvas/DesignCanvas.tsx ---
import React, { useRef, useState } from 'react';
import { useQuestionnaire } from '../../../contexts/QuestionnaireContext'; // Need context to update data
import { useDragDrop } from '../../../contexts/DragDropContext'; // Need context for drag state/validation
import { Control, ControlType } from '../../../types';
import CanvasControl from './CanvasControl'; // Component to render individual controls
const DesignCanvas: React.FC = () => {
// Get functions from QuestionnaireContext to modify form data
const { questionnaire, addControl, moveControl, updateQuestionnaireControls } = useQuestionnaire();
// Get state and functions from DragDropContext for drag/drop logic
const { draggedItem, canDropIn, endDrag, getSourceContainer, getSourceIndex } = useDragDrop();
const canvasRef = useRef<HTMLDivElement>(null); // Ref for the canvas element
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null); // State to track potential drop index
// Handler for when something is dragged over the canvas
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
// Check if an item is being dragged AND if the canvas is a valid drop target for it
if (draggedItem && canDropIn('canvas')) {
e.preventDefault(); // Prevent default browser drag behavior (like opening the item)
// Logic here determines the dragOverIndex based on mouse position
// and updates state to show visual indicator (e.g., line between controls)
// ... simplified for example ...
if (!draggedItem.isNew) { // Only calculate position for existing controls
// Find where the mouse is relative to controls on the canvas
// This would involve iterating through rendered controls and checking mouse position
// For simplicity, imagine this calculates and sets dragOverIndex
// setDragOverIndex(calculatedIndex);
} else {
// For new items, maybe just highlight the whole canvas or indicate append
setDragOverIndex(questionnaire.controls.length); // Example: always suggest appending
}
}
};
// Handler for when something is dropped on the canvas
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault(); // Prevent default browser drop behavior
e.stopPropagation(); // Stop event from bubbling up
console.log('[DesignCanvas] Drop event triggered');
// Double-check if we have a dragged item and if canvas is valid target
if (draggedItem && canDropIn('canvas')) {
try {
if (draggedItem.isNew) {
console.log('[DesignCanvas] Dropped a new control:', draggedItem.controlType);
// Prepare the data for the new control
const controlData: Partial<Control> = {
type: draggedItem.controlType,
label: `New ${draggedItem.controlType}`,
// ... add other default properties based on type ...
};
// Call the function from QuestionnaireContext to add the control
addControl(controlData);
// If dragOverIndex was determined, you might move it after adding
// The actual DesignCanvas code does this:
// addControl(controlData); // Adds to end
// const newControlId = questionnaire.controls[questionnaire.controls.length - 1].id;
// moveControl(newControlId, 'canvas', 'canvas', dragOverIndex); // Then moves it
} else {
console.log('[DesignCanvas] Dropped an existing control:', draggedItem.id);
// This is for reordering existing controls on the canvas
// Use moveControl from QuestionnaireContext to change its position
// The actual index logic can be complex depending on nested containers
// For canvas, it's moving within the top-level 'controls' array
const sourceContainer = getSourceContainer(); // e.g., 'canvas'
const sourceIndex = getSourceIndex(); // e.g., 3 (if it was the 4th item)
const targetContainer = 'canvas'; // Dropping onto the canvas
const targetIndex = dragOverIndex !== null ? dragOverIndex : questionnaire.controls.length; // Where to drop
if (sourceContainer && sourceIndex !== null) {
// Call the moveControl function from QuestionnaireContext
// The actual implementation in QuestionnaireContext handles the array manipulation
moveControl(draggedItem.id, sourceContainer, targetContainer, targetIndex);
console.log(`[DesignCanvas] Requested move control ${draggedItem.id} from ${sourceContainer}[${sourceIndex}] to ${targetContainer}[${targetIndex}]`);
} else {
console.warn('[DesignCanvas] Cannot move existing control: Missing source info.');
}
}
// Clear the drag state in the context now that the drop is handled
setDragOverIndex(null); // Clear visual indicator state
endDrag(); // Call endDrag in the context
// ... also clear visual indicators on the UI ...
} catch (error) {
console.error('[DesignCanvas] Error in canvas drop:', error);
}
}
};
// Handler for when the dragged item leaves the canvas area
const handleDragLeave = () => {
// Clear the visual drop indicator
setDragOverIndex(null);
};
return (
// The canvas area is a drop target
<div
ref={canvasRef}
className={`... ${draggedItem && canDropIn('canvas') ? 'border-blue-400 bg-blue-50' : '...'}`} // Change style if valid drop target
onDragOver={handleDragOver} // Listen for drag over events
onDrop={handleDrop} // Listen for drop events
onDragLeave={handleDragLeave} // Listen for drag leave events
>
{/* Render existing controls */}
<div className="space-y-4">
{/*
The DesignCanvas renders the controls from questionnaire.controls.
Each rendered control element (like CanvasControl) is also made draggable
using the useDragDrop hook to allow reordering.
Example rendering loop (simplified):
*/}
{questionnaire.controls.map((control, index) => (
<div
key={control.id}
className={`relative canvas-control-wrapper ${dragOverIndex === index ? 'border-t-2 border-blue-500' : dragOverIndex === index + 1 ? 'border-b-2 border-blue-500' : ''}`}
draggable // Make existing controls draggable
onDragStart={(e) => {
// Start drag for existing control, pass its ID, index, etc.
startDrag({
id: control.id,
type: 'control',
controlType: control.type,
isNew: false,
sourceId: 'canvas', // This drag started on the canvas
sourceIndex: index, // Its original index
});
// ... add visual cues (e.g., hide original element) ...
}}
onDragEnd={() => {
// Clean up visual cues
// ... ensure endDrag() is called after successful drop or cancellation ...
}}
// Listen for drag over here too for specific drop positions *between* controls
onDragOver={(e) => { /* ... calculate dragOverIndex and show line indicator ... */ }}
onDragLeave={(e) => { /* ... clear line indicator ... */ }}
// This is the handler for dropping directly ONTO this control,
// often used for nesting inside containers.
// The main handleDrop on the canvas div is for dropping *between* controls or onto empty canvas.
onDrop={(e) => { /* ... handle drops specifically on this control ... */ }}
>
<CanvasControl control={control} /> {/* Render the control */}
</div>
))}
</div>
{/* ... empty state message ... */}
</div>
);
};
export default DesignCanvas;
The DesignCanvas
component uses useQuestionnaire
to get access to the form data (questionnaire.controls
) and the functions to modify it (addControl
, moveControl
). It also uses useDragDrop
to know if something is being dragged (draggedItem
) and if the canvas is a valid target (canDropIn
). The onDragOver
handler provides visual feedback, and the onDrop
handler is where the core logic lives: it checks if the draggedItem
is new or existing and calls the appropriate QuestionnaireContext
function (addControl
or moveControl
) to update the questionnaire.controls
array. Finally, it calls endDrag
on the DragDropContext
to clean up the drag state. The individual CanvasControl
elements within the canvas are also made draggable
so users can rearrange them.
Component / Concept | Role | How it Interacts |
---|---|---|
Drag Source | The element being dragged (Palette item or Canvas control). | Calls startDrag on DragDropContext , provides item info (id , type , isNew ). |
Drop Target | The area where items can be dropped (Canvas, Container). | Listens for dragover (calls canDropIn for visual cues) and drop (gets draggedItem , calls addControl /moveControl on QuestionnaireContext , calls endDrag ). |
DragDropContext | Manages the state of the current drag operation. | Stores draggedItem , isDragging . Provides startDrag , endDrag , canDropIn . |
QuestionnaireContext | Manages the form data (Questionnaire ). |
Receives calls (addControl , moveControl ) from drop targets to update the form structure. |
Visual Cues | Highlights or indicators showing drop possibility/position. | Controlled by drop targets based on canDropIn and mouse position. |
In this chapter, we explored the Drag and Drop System, the key feature that allows users to visually build and rearrange their forms. We learned how it translates physical mouse actions into meaningful updates to the application's state. Drag sources initiate the process by informing the DragDropContext
what is being dragged. Drop targets listen for drag events, use the DragDropContext
to validate drops and get dragged item information, and finally, call functions on the QuestionnaireContext
to modify the central Questionnaire
data, adding or moving Control instances as needed.
Now that you can add and arrange controls on the canvas, you'll naturally want to change their appearance and behavior. How do you change the label of a Text Box, make a Checkbox required, or add options to a Dropdown? This is handled by the Properties Panel, which we'll explore next.