Drag and drop shape

This example shows how to create custom shapes that can be dragged and dropped onto each other.

import {
	Circle2d,
	Geometry2d,
	HTMLContainer,
	Rectangle2d,
	ShapeUtil,
	TLDragShapesOutInfo,
	TLShape,
	Tldraw,
} from 'tldraw'
import 'tldraw/tldraw.css'

const MY_GRID_SHAPE_TYPE = 'my-grid-shape'
const MY_COUNTER_SHAPE_TYPE = 'my-counter-shape'

// [1]
declare module 'tldraw' {
	export interface TLGlobalShapePropsMap {
		[MY_GRID_SHAPE_TYPE]: Record<string, never>
		[MY_COUNTER_SHAPE_TYPE]: Record<string, never>
	}
}

// [2]
type MyGridShape = TLShape<typeof MY_GRID_SHAPE_TYPE>
type MyCounterShape = TLShape<typeof MY_COUNTER_SHAPE_TYPE>

// [3]
const SLOT_SIZE = 100
class MyCounterShapeUtil extends ShapeUtil<MyCounterShape> {
	static override type = MY_COUNTER_SHAPE_TYPE

	override canResize() {
		return false
	}
	override hideResizeHandles() {
		return true
	}

	getDefaultProps(): MyCounterShape['props'] {
		return {}
	}

	getGeometry(): Geometry2d {
		return new Circle2d({ radius: SLOT_SIZE / 2 - 10, isFilled: true })
	}

	component() {
		return (
			<HTMLContainer
				style={{
					backgroundColor: '#e03131',
					border: '1px solid #ff8787',
					borderRadius: '50%',
				}}
			/>
		)
	}

	indicator() {
		return <circle r={SLOT_SIZE / 2 - 10} cx={SLOT_SIZE / 2 - 10} cy={SLOT_SIZE / 2 - 10} />
	}
}

// [4]
class MyGridShapeUtil extends ShapeUtil<MyGridShape> {
	static override type = MY_GRID_SHAPE_TYPE

	getDefaultProps(): MyGridShape['props'] {
		return {}
	}

	getGeometry(): Geometry2d {
		return new Rectangle2d({
			width: SLOT_SIZE * 5,
			height: SLOT_SIZE * 2,
			isFilled: true,
		})
	}

	override canResize() {
		return false
	}
	override hideResizeHandles() {
		return true
	}

	// [5]
	override onDragShapesIn(shape: MyGridShape, draggingShapes: TLShape[]): void {
		const { editor } = this
		const reparentingShapes = draggingShapes.filter(
			(s) => s.parentId !== shape.id && s.type === 'my-counter-shape'
		)
		if (reparentingShapes.length === 0) return
		editor.reparentShapes(reparentingShapes, shape.id)
	}

	// [6]
	override onDragShapesOut(
		shape: MyGridShape,
		draggingShapes: TLShape[],
		info: TLDragShapesOutInfo
	): void {
		const { editor } = this
		const reparentingShapes = draggingShapes.filter((s) => s.parentId !== shape.id)
		if (!info.nextDraggingOverShapeId) {
			editor.reparentShapes(reparentingShapes, editor.getCurrentPageId())
		}
	}

	component() {
		return (
			<HTMLContainer
				style={{
					backgroundColor: '#efefef',
					borderRight: '1px solid #ccc',
					borderBottom: '1px solid #ccc',
					backgroundSize: `${SLOT_SIZE}px ${SLOT_SIZE}px`,
					backgroundImage: `
						linear-gradient(to right, #ccc 1px, transparent 1px),
						linear-gradient(to bottom, #ccc 1px, transparent 1px)
					`,
				}}
			/>
		)
	}

	indicator() {
		return <rect width={SLOT_SIZE * 5} height={SLOT_SIZE * 2} />
	}
}

export default function DragAndDropExample() {
	return (
		<div className="tldraw__editor">
			<Tldraw
				shapeUtils={[MyGridShapeUtil, MyCounterShapeUtil]}
				onMount={(editor) => {
					if (editor.getCurrentPageShapeIds().size > 0) return
					editor.createShape({ type: 'my-grid-shape', x: 100, y: 100 })
					editor.createShape({ type: 'my-counter-shape', x: 700, y: 100 })
					editor.createShape({ type: 'my-counter-shape', x: 750, y: 200 })
					editor.createShape({ type: 'my-counter-shape', x: 770, y: 300 })
				}}
			/>
		</div>
	)
}

/*
[1]
First, we need to extend TLGlobalShapePropsMap to add our shape's props to the global type system.
This tells TypeScript about the shape's properties. Here we use Record<string, never> since our shapes
don't need any custom properties. These are very basic custom shapes: see the custom shape examples for
more complex examples.

[2]
Define the shape types using TLShape with each shape's type as a type argument.

[3]
Create a ShapeUtil for the counter shape. This defines how the shape behaves and renders. We disable resizing
and use Circle2d geometry for collision detection. The component renders as a red circle using HTMLContainer.

[4]
Create a ShapeUtil for the grid shape. This creates a rectangular grid that can accept dropped shapes. We use
Rectangle2d geometry and render it with CSS grid lines using background gradients.

[5]
Override onDragShapesIn to handle when shapes are dragged into the grid. We filter for counter shapes that
aren't already children of this grid, then reparent them to become children. This makes them move with the grid.

[6]
Override onDragShapesOut to handle when shapes are dragged out of the grid. If they're not being dragged to
another shape, we reparent them back to the page level, making them independent again.
*/
Is this page helpful?
Prev
Data grid shape
Next
Attach shapes together (bindings)