Skip to content

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 name
  • propsSchema - FormKit schema for editable properties
  • eventsSchema - FormKit schema for event handlers
  • defaultProps - Default property values
  • rules - Drag-and-drop behavior rules
  • slots - 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:

typescript
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:

typescript
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 ​

typescript
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:

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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:

typescript
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:

  1. Blueprints define what appears in the editor sidebar
  2. 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)

typescript
// 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)

typescript
// 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 ​

typescript
{
  $formkit: "text",
  name: "title",
  label: "Title",
  validation: "required",
  placeholder: "Enter title...",
}

Textarea ​

typescript
{
  $formkit: "textarea",
  name: "description",
  label: "Description",
  rows: 4,
}

Number ​

typescript
{
  $formkit: "number",
  name: "fontSize",
  label: "Font Size",
  value: 16,
  min: 12,
  max: 72,
}

Color ​

typescript
{
  $formkit: "color",
  name: "backgroundColor",
  label: "Background Color",
  value: "#ffffff",
}

Select ​

typescript
{
  $formkit: "select",
  name: "align",
  label: "Alignment",
  options: [
    { value: "left", label: "Left" },
    { value: "center", label: "Center" },
    { value: "right", label: "Right" },
  ],
  value: "left",
}

URL ​

typescript
{
  $formkit: "url",
  name: "imageUrl",
  label: "Image URL",
  validation: "required|url",
}

📚 See Also:

Resolver Best Practices ​

1. Always Provide Default Props ​

typescript
// ❌ 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 ​

typescript
// ❌ Bad - no validation
{
  $formkit: "text",
  name: "email",
  label: "Email",
}

// ✅ Good - with validation
{
  $formkit: "text",
  name: "email",
  label: "Email",
  validation: "required|email",
}
typescript
// ✅ 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 ​

typescript
// ✅ Define all available slots
{
  componentName: "MyContainer",
  slots: ["header", "body", "footer"]
}

5. Use Rules for Complex Drag-and-Drop Behavior ​

typescript
// ✅ 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:

typescript
componentName: "MyButton" // But your Vue file is Button.vue

✅ Correct:

typescript
componentName: "Button" // Must match your Vue component name

Mistake 2: Missing Default Props ​

❌ Wrong:

typescript
{
  componentName: "MyComponent",
  propsSchema: [...]
  // No defaultProps!
}

✅ Correct:

typescript
{
  componentName: "MyComponent",
  propsSchema: [...],
  defaultProps: {
    title: "Default Title",
    color: "#333333"
  }
}

Mistake 3: Incorrect FormKit Schema Syntax ​

❌ Wrong:

typescript
propsSchema: [
  {
    type: "text",  // Should be $formkit
    name: "title",
    label: "Title"
  }
]

✅ Correct:

typescript
propsSchema: [
  {
    $formkit: "text",  // Correct FormKit syntax
    name: "title",
    label: "Title"
  }
]

Mistake 4: Not Defining Slots for Container Components ​

❌ Wrong:

typescript
{
  componentName: "MyContainer",
  propsSchema: [...],
  // No slots definition!
}

✅ Correct:

typescript
{
  componentName: "MyContainer",
  propsSchema: [...],
  slots: ["default", "header", "footer"]
}

Testing Your Resolvers ​

Quick Test Method ​

typescript
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:

  1. Learn about Blueprints to see how resolvers connect to the editor UI
  2. Create Data Wrappers for dynamic content