@tldraw/tlschema
Version:
tldraw infinite canvas SDK (schema).
190 lines (185 loc) • 5.88 kB
text/typescript
import { JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator'
import { TLBindingId } from '../records/TLBinding'
import { TLShapeId } from '../records/TLShape'
import { shapeIdValidator } from '../shapes/TLBaseShape'
/**
* Base interface for all binding types in tldraw. Bindings represent relationships
* between shapes, such as arrows connecting to other shapes or organizational connections.
*
* All default bindings extend this base interface with specific type and property definitions.
* The binding system enables shapes to maintain relationships that persist through
* transformations, movements, and other operations.
*
* Custom bindings should be defined by augmenting the TLGlobalBindingPropsMap type and getting the binding type from the TLBinding type.
*
* @param Type - String literal type identifying the specific binding type (e.g., 'arrow')
* @param Props - Object containing binding-specific properties and configuration
*
* @example
* ```ts
* // Define a default binding type
* interface TLArrowBinding extends TLBaseBinding<'arrow', TLArrowBindingProps> {}
*
* interface TLArrowBindingProps {
* terminal: 'start' | 'end'
* normalizedAnchor: VecModel
* isExact: boolean
* isPrecise: boolean
* snap: ElbowArrowSnap
* }
*
* // Create a binding instance
* const arrowBinding: TLArrowBinding = {
* id: 'binding:abc123',
* typeName: 'binding',
* type: 'arrow',
* fromId: 'shape:source1',
* toId: 'shape:target1',
* props: {
* terminal: 'end',
* normalizedAnchor: { x: 0.5, y: 0.5 },
* isExact: false,
* isPrecise: true,
* snap: 'edge'
* },
* meta: {}
* }
* ```
*
* @public
*/
export interface TLBaseBinding<Type extends string, Props extends object> {
// using real `extends BaseRecord<'binding', TLBindingId>` introduces a circularity in the types
// and for that reason those "base members" have to be declared manually here
readonly id: TLBindingId
readonly typeName: 'binding'
/** The specific type of this binding (e.g., 'arrow', 'custom') */
type: Type
/** ID of the source shape in this binding relationship */
fromId: TLShapeId
/** ID of the target shape in this binding relationship */
toId: TLShapeId
/** Binding-specific properties that define behavior and appearance */
props: Props
/** User-defined metadata for extending binding functionality */
meta: JsonObject
}
/**
* Validator for binding IDs. Ensures that binding identifiers follow the correct
* format and type constraints required by the tldraw schema system.
*
* Used internally by the schema validation system to verify binding IDs when
* records are created or modified. All binding IDs must be prefixed with 'binding:'.
*
* @example
* ```ts
* import { bindingIdValidator } from '@tldraw/tlschema'
*
* // Validate a binding ID
* const isValid = bindingIdValidator.isValid('binding:abc123') // true
* const isInvalid = bindingIdValidator.isValid('shape:abc123') // false
*
* // Use in custom validation schema
* const customBindingValidator = T.object({
* id: bindingIdValidator,
* // ... other properties
* })
* ```
*
* @public
*/
export const bindingIdValidator = idValidator<TLBindingId>('binding')
/**
* Creates a runtime validator for a specific binding type. This factory function
* generates a complete validation schema for custom bindings that extends TLBaseBinding.
*
* The validator ensures all binding records conform to the expected structure with
* proper type safety and runtime validation. It validates the base binding properties
* (id, type, fromId, toId) along with custom props and meta fields.
*
* @param type - The string literal type identifier for this binding (e.g., 'arrow', 'custom')
* @param props - Optional validation schema for binding-specific properties
* @param meta - Optional validation schema for metadata fields
*
* @returns A validator object that can validate complete binding records
*
* @example
* ```ts
* import { createBindingValidator } from '@tldraw/tlschema'
* import { T } from '@tldraw/validate'
*
* // Create validator for a custom binding type
* const myBindingValidator = createBindingValidator(
* 'myBinding',
* {
* strength: T.number,
* color: T.string,
* enabled: T.boolean
* },
* {
* createdAt: T.number,
* author: T.string
* }
* )
*
* // Validate a binding instance
* const bindingData = {
* id: 'binding:123',
* typeName: 'binding',
* type: 'myBinding',
* fromId: 'shape:abc',
* toId: 'shape:def',
* props: {
* strength: 0.8,
* color: 'red',
* enabled: true
* },
* meta: {
* createdAt: Date.now(),
* author: 'user123'
* }
* }
*
* const isValid = myBindingValidator.isValid(bindingData) // true
* ```
*
* @example
* ```ts
* // Simple binding without custom props or meta
* const simpleBindingValidator = createBindingValidator('simple')
*
* // This will use JsonValue validation for props and meta
* const binding = {
* id: 'binding:456',
* typeName: 'binding',
* type: 'simple',
* fromId: 'shape:start',
* toId: 'shape:end',
* props: {}, // Any JSON value allowed
* meta: {} // Any JSON value allowed
* }
* ```
*
* @public
*/
export function createBindingValidator<
Type extends string,
Props extends JsonObject,
Meta extends JsonObject,
>(
type: Type,
props?: { [K in keyof Props]: T.Validatable<Props[K]> },
meta?: { [K in keyof Meta]: T.Validatable<Meta[K]> }
) {
return T.object<TLBaseBinding<Type, Props>>({
id: bindingIdValidator,
typeName: T.literal('binding'),
type: T.literal(type),
fromId: shapeIdValidator,
toId: shapeIdValidator,
props: props ? T.object(props) : (T.jsonValue as any),
meta: meta ? T.object(meta) : (T.jsonValue as any),
})
}