Resolvers: The Component Registry
Think of resolvers as a registry that tells v-craft everything about your components. While blueprints define how components appear in the editor sidebar, resolvers define the actual component behavior, editable properties, event handlers, and rules.
What Are Resolvers?
Resolvers are like component registries that tell the editor:
- What components actually are
- What properties can be edited
- What events can be handled
- What drag-and-drop rules apply
- What slots are available
🔑 Key Concept: Resolver Map A resolver map is a JavaScript object where keys are component names and values contain all the metadata about each component. This metadata includes:
componentName- The actual Vue component namepropsSchema- FormKit schema for editable propertieseventsSchema- FormKit schema for event handlersdefaultProps- Default property valuesrules- Drag-and-drop behavior rulesslots- Available slot names
🎯 Built on FormKit Resolvers use FormKit schemas for defining editable properties and events. This means you get access to FormKit's powerful form system, validation, and field types.
The Simplest Resolver Ever
Let's start with the most basic resolver possible:
const myResolver = {
SimpleText: {
componentName: "SimpleText",
propsSchema: [
{
$formkit: "text",
name: "text",
label: "Text Content",
}
],
defaultProps: {
text: "Hello World!"
}
}
}That's it! This resolver tells v-craft:
- There's a component called "SimpleText"
- It has one editable property called "text"
- The default value is "Hello World!"
Creating Your First Resolver
Step 1: Define the Resolver Map
Create a file called resolvers.ts:
import type { CraftNodeResolverMap } from '@versa-stack/v-craft'
export const myResolvers: CraftNodeResolverMap<any> = {
HeroSection: {
componentName: "HeroSection",
propsSchema: [
{
$formkit: "text",
name: "title",
label: "Hero Title",
validation: "required",
},
{
$formkit: "textarea",
name: "subtitle",
label: "Subtitle",
},
{
$formkit: "color",
name: "backgroundColor",
label: "Background Color",
value: "#6366f1",
}
],
eventsSchema: {
$el: "div",
children: [
{
$formkit: "textarea",
name: "onClick",
label: "onClick Handler",
},
],
},
defaultProps: {
title: "Welcome to Our Site",
subtitle: "We make amazing things",
backgroundColor: "#6366f1",
}
},
InfoCard: {
componentName: "InfoCard",
propsSchema: [
{
$formkit: "text",
name: "title",
label: "Card Title",
},
{
$formkit: "textarea",
name: "description",
label: "Description",
},
{
$formkit: "url",
name: "imageUrl",
label: "Image URL",
}
],
defaultProps: {
title: "Card Title",
description: "This is a description",
imageUrl: "https://via.placeholder.com/300x200",
}
}
}Step 2: Use Your Resolvers in the Editor
import { CraftNodeResolver } from '@versa-stack/v-craft'
import { myResolvers } from './resolvers'
const resolver = new CraftNodeResolver(myResolvers)
const editorConfig = {
resolver
}Resolver Structure Explained
Let's break down every part of a resolver:
const myResolver = {
// 1. COMPONENT NAME - Must match your Vue component
componentName: "MyVueComponent",
// 2. PROPS SCHEMA - FormKit schema for editable properties
propsSchema: [
{
$formkit: "text",
name: "title",
label: "Title",
validation: "required",
},
{
$formkit: "number",
name: "fontSize",
label: "Font Size",
value: 16,
}
],
// 3. EVENTS SCHEMA - FormKit schema for event handlers
eventsSchema: {
$el: "div",
children: [
{
$formkit: "textarea",
name: "onClick",
label: "onClick Handler",
},
],
},
// 4. DEFAULT PROPS - Default property values
defaultProps: {
title: "Default Title",
fontSize: 16,
},
// 5. RULES - Drag-and-drop behavior rules
rules: {
canAccept: (node) => true,
canDrag: (node) => true,
},
// 6. SLOTS - Available slot names
slots: ["default", "header", "footer"]
}Resolver Examples for Every Situation
1. Simple Text Component
const TextBlock = {
componentName: "TextBlock",
propsSchema: [
{
$formkit: "text",
name: "text",
label: "Text Content",
},
{
$formkit: "number",
name: "fontSize",
label: "Font Size (px)",
value: 16,
},
{
$formkit: "color",
name: "color",
label: "Text Color",
value: "#333333",
},
{
$formkit: "select",
name: "align",
label: "Text Alignment",
options: [
{ value: "left", label: "Left" },
{ value: "center", label: "Center" },
{ value: "right", label: "Right" },
],
value: "left",
}
],
defaultProps: {
text: "Type your text here...",
fontSize: 16,
color: "#333333",
align: "left"
}
}2. Button Component
const Button = {
componentName: "ActionButton",
propsSchema: [
{
$formkit: "text",
name: "text",
label: "Button Text",
validation: "required",
},
{
$formkit: "color",
name: "backgroundColor",
label: "Background Color",
value: "#007bff",
},
{
$formkit: "color",
name: "textColor",
label: "Text Color",
value: "#ffffff",
},
{
$formkit: "number",
name: "borderRadius",
label: "Border Radius (px)",
value: 4,
}
],
eventsSchema: {
$el: "div",
children: [
{
$formkit: "textarea",
name: "onClick",
label: "onClick Handler",
},
],
},
defaultProps: {
text: "Click Me",
backgroundColor: "#007bff",
textColor: "#ffffff",
borderRadius: 4,
}
}3. Image Component
const Image = {
componentName: "DisplayImage",
propsSchema: [
{
$formkit: "url",
name: "src",
label: "Image URL",
validation: "required|url",
},
{
$formkit: "text",
name: "alt",
label: "Alt Text",
},
{
$formkit: "number",
name: "width",
label: "Width (px)",
value: 400,
},
{
$formkit: "number",
name: "height",
label: "Height (px)",
value: 300,
},
{
$formkit: "number",
name: "borderRadius",
label: "Border Radius (px)",
value: 0,
}
],
defaultProps: {
src: "https://via.placeholder.com/400x300",
alt: "Description of image",
width: 400,
height: 300,
borderRadius: 0,
}
}4. Container Component with Slots
const Container = {
componentName: "ContainerBox",
propsSchema: [
{
$formkit: "color",
name: "bgColor",
label: "Background Color",
value: "#f8f9fa",
},
{
$formkit: "number",
name: "padding",
label: "Padding (px)",
value: 20,
},
{
$formkit: "number",
name: "borderRadius",
label: "Border Radius (px)",
value: 8,
}
],
defaultProps: {
bgColor: "#f8f9fa",
padding: 20,
borderRadius: 8,
},
slots: ["default"]
}5. Component with Drag-and-Drop Rules
const DraggableSection = {
componentName: "Section",
propsSchema: [
{
$formkit: "text",
name: "title",
label: "Section Title",
}
],
defaultProps: {
title: "My Section",
},
rules: {
canAccept: (node) => {
// Only accept text and button components
return node.componentName === "TextBlock" || node.componentName === "ActionButton"
},
canDrag: (node) => {
// Allow dragging unless it's the root node
return node.parentUuid !== null
}
}
}Using the CraftNodeResolver Class
The CraftNodeResolver class provides methods to work with your resolver map:
import { CraftNodeResolver } from '@versa-stack/v-craft'
import { myResolvers } from './resolvers'
const resolver = new CraftNodeResolver(myResolvers)
// Resolve a component by name
const componentInfo = resolver.resolve("HeroSection")
// Get default props for a CraftNode
const defaults = resolver.getDefaultProps(myCraftNode)
// Get the props schema for a CraftNode
const schema = resolver.getSchema(myCraftNode)
// Get the events schema for a CraftNode
const eventsSchema = resolver.getEventsSchema(myCraftNode)
// Get drag-and-drop rules for a CraftNode
const rules = resolver.getRules(myCraftNode)How Resolvers and Blueprints Work Together
The Two-Layer System
Your implementation uses a two-layer system where blueprints and resolvers work together:
- Blueprints define what appears in the editor sidebar
- Resolvers define the actual components and their editable properties
Real Example: HTML DIV Container
Looking at your actual code, here's how a <div> becomes a container:
Layer 1: Resolver (defines the actual component)
// resolvermap.ts - defines what a <div> is
const resolveHtmlElements = (elements: string[]) => {
const mapped: Record<string, any> = {};
elements.forEach((element) => {
mapped[element] = {
componentName: element,
eventsSchema: {
$el: "div",
children: [
{
$formkit: "textarea",
name: "click",
label: "onClick",
},
],
},
propsSchema: [
{
$formkit: "text",
label: "CSS Class(es)",
name: "class",
},
],
};
});
return mapped;
};Layer 2: Blueprint (defines what users see in editor)
// blueprints.ts - creates the draggable blueprint
const createHtmlElementBlueprints = () => {
const resolverMap: CraftNodeResolverMap<any> = htmlResolvers;
const blueprints: Blueprints<any> = {};
Object.entries(resolverMap).forEach(([key, value]) => {
blueprints[key] = {
label: `HTML <${value.componentName}>`,
componentName: "CraftCanvas",
props: {
...value.defaultProps,
componentName: value.componentName,
},
slots: {},
};
});
return blueprints;
};Why This Separation Works
- Resolvers define the actual component behavior and properties
- Blueprints define how components appear in the editor
- This allows the same component to have different editor representations
- Container behavior is added by CraftCanvas, not the component itself
FormKit Schema Reference
All resolver schemas use FormKit schemas. Here are common patterns:
Text Input
{
$formkit: "text",
name: "title",
label: "Title",
validation: "required",
placeholder: "Enter title...",
}Textarea
{
$formkit: "textarea",
name: "description",
label: "Description",
rows: 4,
}Number
{
$formkit: "number",
name: "fontSize",
label: "Font Size",
value: 16,
min: 12,
max: 72,
}Color
{
$formkit: "color",
name: "backgroundColor",
label: "Background Color",
value: "#ffffff",
}Select
{
$formkit: "select",
name: "align",
label: "Alignment",
options: [
{ value: "left", label: "Left" },
{ value: "center", label: "Center" },
{ value: "right", label: "Right" },
],
value: "left",
}URL
{
$formkit: "url",
name: "imageUrl",
label: "Image URL",
validation: "required|url",
}📚 See Also:
- FormKit Input Documentation - Complete list of all available input types
- FormKit Validation - Advanced validation options
- FormKit Conditional Logic - Show/hide fields based on other values
Resolver Best Practices
1. Always Provide Default Props
// ❌ Bad - no defaults
const BadResolver = {
componentName: "MyComponent",
propsSchema: [...]
}
// ✅ Good - always provide defaults
const GoodResolver = {
componentName: "MyComponent",
propsSchema: [...],
defaultProps: {
title: "Default Title",
color: "#333333"
}
}2. Use Validation Where Appropriate
// ❌ Bad - no validation
{
$formkit: "text",
name: "email",
label: "Email",
}
// ✅ Good - with validation
{
$formkit: "text",
name: "email",
label: "Email",
validation: "required|email",
}3. Group Related Properties
// ✅ Group related properties in your schema
propsSchema: [
// Content properties
{ $formkit: "text", name: "title", label: "Title" },
{ $formkit: "textarea", name: "description", label: "Description" },
// Styling properties
{ $formkit: "color", name: "backgroundColor", label: "Background" },
{ $formkit: "color", name: "textColor", label: "Text Color" },
]4. Define Slots for Multi-Slot Components
// ✅ Define all available slots
{
componentName: "MyContainer",
slots: ["header", "body", "footer"]
}5. Use Rules for Complex Drag-and-Drop Behavior
// ✅ Define rules to control drag-and-drop
rules: {
canAccept: (node) => {
// Custom logic for what can be dropped
return true
},
canDrag: (node) => {
// Custom logic for what can be dragged
return true
}
}Common Resolver Mistakes (And How to Fix Them)
Mistake 1: Wrong Component Name
❌ Wrong:
componentName: "MyButton" // But your Vue file is Button.vue✅ Correct:
componentName: "Button" // Must match your Vue component nameMistake 2: Missing Default Props
❌ Wrong:
{
componentName: "MyComponent",
propsSchema: [...]
// No defaultProps!
}✅ Correct:
{
componentName: "MyComponent",
propsSchema: [...],
defaultProps: {
title: "Default Title",
color: "#333333"
}
}Mistake 3: Incorrect FormKit Schema Syntax
❌ Wrong:
propsSchema: [
{
type: "text", // Should be $formkit
name: "title",
label: "Title"
}
]✅ Correct:
propsSchema: [
{
$formkit: "text", // Correct FormKit syntax
name: "title",
label: "Title"
}
]Mistake 4: Not Defining Slots for Container Components
❌ Wrong:
{
componentName: "MyContainer",
propsSchema: [...],
// No slots definition!
}✅ Correct:
{
componentName: "MyContainer",
propsSchema: [...],
slots: ["default", "header", "footer"]
}Testing Your Resolvers
Quick Test Method
import { CraftNodeResolver } from '@versa-stack/v-craft'
import { myResolvers } from './resolvers'
const resolver = new CraftNodeResolver(myResolvers)
// Test resolution
console.log('Testing HeroSection resolver:', {
componentName: resolver.resolve("HeroSection")?.componentName,
hasPropsSchema: !!resolver.resolve("HeroSection")?.propsSchema,
hasDefaultProps: !!resolver.resolve("HeroSection")?.defaultProps,
hasEventsSchema: !!resolver.resolve("HeroSection")?.eventsSchema,
})Next Steps
Now that you understand resolvers:
- Learn about Blueprints to see how resolvers connect to the editor UI
- Create Data Wrappers for dynamic content