# Dragble Integration

> Add the Dragble drag-and-drop editor to React, Vue, Angular, or vanilla JavaScript. Covers installation, component setup, lifecycle events, multiple instances, TypeScript types, and SDK/editor versioning.

- Skill: `dragble/dragble-integration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dragble/dragble-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dragble/dragble-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: Dragble (https://skillmd.com/u/dragble)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dragble/dragble-integration

---


# Dragble Editor Integration

## Overview

Dragble is an embeddable drag-and-drop email/web/popup editor. You integrate it into your application using either a **framework wrapper** (React, Vue, Angular) or the **vanilla JavaScript SDK** loaded from CDN.

### Packages

| Package | Install | Purpose |
|---|---|---|
| `dragble-react-editor` | `npm install dragble-react-editor` | React wrapper component |
| `dragble-vue-editor` | `npm install dragble-vue-editor` | Vue 3 wrapper component |
| `dragble-angular-editor` | `npm install dragble-angular-editor` | Angular wrapper component |
| `dragble-types` | `npm install dragble-types` | Shared TypeScript type definitions |

**The SDK itself (`@dragble/editor-sdk`) is CDN-only.** It is loaded automatically by the framework wrappers from `https://sdk.dragble.com/{version}/dragble-sdk.min.js`. There is NO npm package for the SDK. NEVER suggest `npm install @dragble/editor-sdk`.

### Key Concepts

- **editorKey** (required): Authentication key for your project. Format: `ek_*` or `px_*`.
- **containerId**: Auto-generated by wrappers. Required when using vanilla JS.
- **Editor modes**: `"email"` (default), `"web"`, `"popup"`.
- **Design modes**: `"edit"` (admin -- shows Row Actions for setting permissions), `"live"` (end-user -- enforces permissions). Default: `"live"`.
- All async methods return **Promises**. There are no callback-based async APIs.

---

## React

### Installation

```bash
npm install dragble-react-editor
```

Types are included automatically via re-export from `dragble-types`.

### Complete Example

```tsx
import { useRef, useState } from "react";
import { DragbleEditor, type DragbleEditorRef, type DesignJson } from "dragble-react-editor";

export default function EmailEditor() {
  const editorRef = useRef<DragbleEditorRef>(null);
  const [lastSaved, setLastSaved] = useState<string | null>(null);

  const handleReady = (editor) => {
    console.log("Editor ready");
  };

  const handleSave = async () => {
    const editor = editorRef.current?.editor;
    if (!editor) return;

    // Export HTML
    const html = await editor.exportHtml();
    console.log("HTML:", html);

    // Export JSON (for saving/restoring designs)
    const json = await editor.exportJson();
    console.log("JSON:", json);
    setLastSaved(new Date().toISOString());
  };

  const handleLoad = () => {
    const editor = editorRef.current?.editor;
    if (!editor) return;

    // Load a previously saved design
    const savedDesign: DesignJson = JSON.parse(localStorage.getItem("design")!);
    editor.loadDesign(savedDesign);
  };

  return (
    <div>
      <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
        <button onClick={handleSave}>Save</button>
        <button onClick={handleLoad}>Load Saved</button>
      </div>
      <DragbleEditor
        ref={editorRef}
        editorKey="ek_your_editor_key"
        editorMode="email"
        height="800px"
        onReady={handleReady}
        onChange={(data) => console.log("Design changed:", data.type)}
        onError={(err) => console.error("Editor error:", err)}
      />
    </div>
  );
}
```

### Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `editorKey` | `string` | **required** | Authentication key (`ek_*` or `px_*`) |
| `editorMode` | `"email" \| "web" \| "popup"` | `"email"` | Editor type |
| `design` | `DesignJson \| ModuleData \| null` | `undefined` | Initial design to load |
| `designMode` | `"edit" \| "live"` | `"live"` | Template permission mode |
| `contentType` | `"module"` | `undefined` | Lock to single-row module editing |
| `options` | `EditorOptions` | `{}` | All editor configuration (appearance, tools, features, AI, storage, etc.) |
| `callbacks` | `DragbleCallbacks` (minus onReady/onLoad/onChange/onError) | `undefined` | SDK callbacks (linkClick, onModuleSave, onPreview, etc.) |
| `popup` | `PopupConfig` | `undefined` | Popup configuration (when editorMode is `"popup"`) |
| `collaboration` | `boolean \| CollaborationFeaturesConfig` | `false` | Enable collaboration/commenting |
| `user` | `UserInfo` | `undefined` | User identity for sessions and collaboration |
| `height` | `string \| number` | `undefined` | Editor height (CSS value or pixels) |
| `minHeight` | `string \| number` | `"600px"` | Minimum height |
| `className` | `string` | `undefined` | CSS class on the outer wrapper |
| `style` | `React.CSSProperties` | `undefined` | Inline styles on the outer wrapper |
| `onReady` | `(editor: DragbleSDK) => void` | `undefined` | Fires once when editor is fully loaded |
| `onLoad` | `() => void` | `undefined` | Fires when a design is loaded |
| `onChange` | `(data: { design: DesignJson; type: string }) => void` | `undefined` | Fires on every design change |
| `onError` | `(error: Error) => void` | `undefined` | Fires on SDK/loading errors |
| `onComment` | `(action: CommentAction) => void` | `undefined` | Fires on comment create/edit/delete/resolve/reopen |
| `sdkUrl` | `string` | CDN latest | Custom SDK script URL (overrides sdkVersion) |
| `sdkVersion` | `string` | `"latest"` | SDK version channel: `"latest"`, `"stable"`, or `"vX.Y.Z"` |
| `editorVersion` | `string` | `undefined` | Editor version forwarded to SDK init config |
| `editorUrl` | `string` | `undefined` | Editor URL override (for self-hosted) |

### Ref API (`DragbleEditorRef`)

```tsx
const editorRef = useRef<DragbleEditorRef>(null);

// Access the SDK instance
const editor = editorRef.current?.editor;

// Check if the editor is ready
const ready = editorRef.current?.isReady();
```

The `editor` property exposes the full `DragbleSDK` interface. All methods are available after `isReady()` returns `true`.

### Hook: `useDragbleEditor`

```tsx
import { useDragbleEditor } from "dragble-react-editor";

function MyComponent() {
  const { ref, editor, isReady } = useDragbleEditor();

  return (
    <DragbleEditor ref={ref} editorKey="ek_..." />
  );
}
```

---

## Vue 3

### Installation

```bash
npm install dragble-vue-editor
```

Types are included automatically via re-export from `dragble-types`.

### Complete Example

```vue
<template>
  <div>
    <div style="display: flex; gap: 8px; margin-bottom: 8px">
      <button @click="handleSave">Save</button>
      <button @click="handleLoadBlank">Load Blank</button>
    </div>
    <DragbleEditor
      ref="editorRef"
      editor-key="ek_your_editor_key"
      editor-mode="email"
      height="800px"
      @ready="onReady"
      @change="onChange"
      @error="onError"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { DragbleEditor, type DragbleSDK, type DesignJson } from "dragble-vue-editor";

const editorRef = ref<InstanceType<typeof DragbleEditor>>();

const onReady = (editor: DragbleSDK) => {
  console.log("Editor ready");
};

const onChange = (data: { design: DesignJson; type: string }) => {
  console.log("Changed:", data.type);
};

const onError = (error: Error) => {
  console.error("Editor error:", error);
};

const handleSave = async () => {
  const html = await editorRef.value?.exportHtml();
  const json = await editorRef.value?.exportJson();
  console.log("Exported:", { html, json });
};

const handleLoadBlank = () => {
  editorRef.value?.loadBlank();
};
</script>
```

### Props

Vue props use **kebab-case** in templates. All the same configuration options as React apply:

| Prop (kebab-case) | Type | Default | Description |
|---|---|---|---|
| `editor-key` | `string` | **required** | Authentication key |
| `editor-mode` | `"email" \| "web" \| "popup"` | `"email"` | Editor type |
| `design` | `DesignJson \| ModuleData \| null` | `undefined` | Initial design |
| `design-mode` | `"edit" \| "live"` | `undefined` | Template permission mode |
| `content-type` | `"module"` | `undefined` | Single-row module editing |
| `options` | `EditorOptions` | `undefined` | All editor configuration |
| `callbacks` | `DragbleCallbacks` (minus lifecycle) | `undefined` | SDK callbacks |
| `popup` | `PopupConfig` | `undefined` | Popup config |
| `collaboration` | `boolean \| CollaborationFeaturesConfig` | `undefined` | Collaboration features |
| `user` | `UserInfo` | `undefined` | User identity |
| `height` | `string \| number` | `"600px"` | Editor height |
| `min-height` | `string \| number` | `"600px"` | Minimum height |
| `merge-tags` | `(MergeTag \| MergeTagGroup)[]` | `undefined` | Personalization tags |
| `modules` | `Module[]` | `undefined` | Custom modules |
| `display-conditions` | `DisplayConditionsConfig` | `undefined` | Display conditions |
| `appearance` | `AppearanceConfig` | `undefined` | Visual customization |
| `tools` | `ToolsConfig` | `undefined` | Tool enable/disable |
| `features` | `FeaturesConfig` | `undefined` | Feature toggles |
| `fonts` | `FontsConfig` | `undefined` | Custom fonts |
| `locale` | `string` | `undefined` | UI language locale |
| `text-direction` | `"ltr" \| "rtl"` | `undefined` | Text direction |
| `body-values` | `Record<string, unknown>` | `undefined` | Default body/canvas values |
| `header` | `object` | `undefined` | Locked header row JSON |
| `footer` | `object` | `undefined` | Locked footer row JSON |
| `ai` | `AIConfig` | `undefined` | AI features configuration |
| `custom-css` | `string[]` | `undefined` | Custom CSS URLs/inline |
| `custom-js` | `string[]` | `undefined` | Custom JS URLs/inline |
| `sdk-url` | `string` | CDN latest | Custom SDK script URL |
| `sdk-version` | `string` | `"latest"` | SDK version channel |
| `editor-version` | `string` | `undefined` | Editor version |
| `editor-url` | `string` | `undefined` | Editor URL override |

Vue also exposes many props as **top-level** (e.g., `merge-tags`, `appearance`, `tools`, `fonts`) in addition to the `options` prop. Top-level props are merged into `options` internally. Either approach works; do not set the same option in both places.

### Emits

| Event | Payload | Description |
|---|---|---|
| `ready` | `DragbleSDK` | Editor is fully loaded |
| `load` | `unknown` | Design was loaded |
| `change` | `{ design: DesignJson; type: string }` | Design changed |
| `error` | `Error` | Error occurred |
| `comment` | `CommentAction` | Comment event |

### Reactive Watchers

The Vue component automatically watches for changes to these props and updates the editor at runtime without re-initialization:

- `design` -- calls `loadDesign()` when changed
- `merge-tags` -- calls `setMergeTags()` when changed
- `modules` -- calls `setModules()` when changed
- `display-conditions` -- calls `setDisplayConditions()` when changed

### Component Ref Methods

All SDK methods are exposed via `expose()` on the component ref:

```vue
<script setup lang="ts">
const editorRef = ref<InstanceType<typeof DragbleEditor>>();

// After ready:
await editorRef.value?.exportHtml();
await editorRef.value?.exportJson();
await editorRef.value?.exportImage();
await editorRef.value?.exportPdf();
editorRef.value?.loadDesign(design);
editorRef.value?.loadBlank();
editorRef.value?.undo();
editorRef.value?.redo();
editorRef.value?.showPreview("desktop");
editorRef.value?.setMergeTags({ customMergeTags: [...] });
editorRef.value?.setModules([...]);
editorRef.value?.addEventListener("row:selected", (data) => { ... });
// ... all DragbleSDK methods are available
</script>
```

### Composable: `useDragbleEditor`

For direct SDK access without the component:

```vue
<script setup lang="ts">
import { useDragbleEditor } from "dragble-vue-editor";

const { editor, isReady, containerId } = useDragbleEditor({
  editorKey: "ek_your_editor_key",
  editorMode: "email",
});

const handleExport = async () => {
  if (isReady.value && editor.value) {
    const html = await editor.value.exportHtml();
    console.log(html);
  }
};
</script>

<template>
  <div :id="containerId" style="height: 600px" />
  <button @click="handleExport" :disabled="!isReady">Export</button>
</template>
```

---

## Angular

### Installation

```bash
npm install dragble-angular-editor
```

Types are included automatically via re-export from `dragble-types`.

### Complete Example (Standalone Component)

```typescript
import { Component, ViewChild } from "@angular/core";
import { DragbleEditorComponent } from "dragble-angular-editor";

@Component({
  selector: "app-email-editor",
  standalone: true,
  imports: [DragbleEditorComponent],
  template: `
    <div>
      <div style="display: flex; gap: 8px; margin-bottom: 8px">
        <button (click)="handleSave()">Save</button>
        <button (click)="handleLoadBlank()">Load Blank</button>
      </div>
      <dragble-editor
        #editor
        editorKey="ek_your_editor_key"
        editorMode="email"
        height="800px"
        (ready)="onReady($event)"
        (change)="onChange($event)"
        (error)="onError($event)"
      ></dragble-editor>
    </div>
  `,
})
export class EmailEditorComponent {
  @ViewChild("editor") editor!: DragbleEditorComponent;

  onReady(sdk: any) {
    console.log("Editor ready");
  }

  onChange(data: { design: any; type: string }) {
    console.log("Changed:", data.type);
  }

  onError(error: Error) {
    console.error("Editor error:", error);
  }

  async handleSave() {
    const html = await this.editor.exportHtml();
    const json = await this.editor.exportJson();
    console.log("Exported:", { html, json });
  }

  handleLoadBlank() {
    this.editor.loadBlank();
  }
}
```

### NgModule Pattern (Legacy)

For Angular apps not using standalone components:

```typescript
import { NgModule } from "@angular/core";
import { DragbleEditorModule } from "dragble-angular-editor";

@NgModule({
  imports: [DragbleEditorModule],
})
export class AppModule {}
```

Then use `<dragble-editor>` in your templates the same way.

### Inputs

| Input | Type | Default | Description |
|---|---|---|---|
| `editorKey` | `string` | **required** | Authentication key |
| `editorMode` | `EditorMode` | `"email"` | Editor type |
| `design` | `DesignJson \| ModuleData \| null` | `undefined` | Initial design |
| `designMode` | `"edit" \| "live"` | `undefined` | Template permission mode |
| `contentType` | `"module"` | `undefined` | Single-row module editing |
| `options` | `Partial<EditorOptions>` | `undefined` | All editor configuration |
| `callbacks` | `DragbleCallbacks` (minus lifecycle) | `undefined` | SDK callbacks |
| `popup` | `PopupConfig` | `undefined` | Popup config |
| `collaboration` | `boolean \| CollaborationFeaturesConfig` | `undefined` | Collaboration features |
| `user` | `UserInfo` | `undefined` | User identity |
| `height` | `string \| number` | `"600px"` | Editor height |
| `minHeight` | `string \| number` | `"600px"` | Minimum height |
| `appearance` | `AppearanceConfig` | `undefined` | Visual customization |
| `tools` | `ToolsConfig` | `undefined` | Tool enable/disable |
| `features` | `FeaturesConfig` | `undefined` | Feature toggles |
| `fonts` | `FontsConfig` | `undefined` | Custom fonts |
| `mergeTags` | `MergeTagsConfig` | `undefined` | Personalization tags |
| `specialLinks` | `SpecialLinksConfig` | `undefined` | Special link categories |
| `modules` | `Module[]` | `undefined` | Custom modules |
| `displayConditions` | `DisplayConditionsConfig` | `undefined` | Display conditions |
| `locale` | `string` | `undefined` | UI language locale |
| `textDirection` | `TextDirection` | `undefined` | Text direction |
| `language` | `Language` | `undefined` | Multi-language config |
| `bodyValues` | `Record<string, unknown>` | `undefined` | Default body values |
| `header` | `unknown` | `undefined` | Locked header row JSON |
| `footer` | `unknown` | `undefined` | Locked footer row JSON |
| `ai` | `AIConfig` | `undefined` | AI features configuration |
| `customCSS` | `string[]` | `undefined` | Custom CSS |
| `customJS` | `string[]` | `undefined` | Custom JS |
| `sdkUrl` | `string` | CDN latest | Custom SDK URL |
| `sdkVersion` | `string` | `"latest"` | SDK version channel |
| `editorVersion` | `string` | `undefined` | Editor version |
| `editorUrl` | `string` | `undefined` | Editor URL override |

### Outputs

| Output | Payload | Description |
|---|---|---|
| `ready` | `DragbleSDK` | Editor is fully loaded |
| `load` | `unknown` | Design was loaded |
| `change` | `{ design: DesignJson; type: string }` | Design changed |
| `error` | `Error` | Error occurred |
| `commentAction` | `CommentAction` | Comment event |

### Change Detection

The Angular component implements `OnChanges`. When these inputs change after initialization, the editor updates automatically:

- `design` -- calls `loadDesign()`
- `mergeTags` -- calls `setMergeTags()`
- `modules` -- calls `setModules()`
- `displayConditions` -- calls `setDisplayConditions()`

### Public Methods

All SDK methods are exposed as public methods on the component:

```typescript
@ViewChild("editor") editor!: DragbleEditorComponent;

// Design
this.editor.loadDesign(design);
this.editor.loadBlank();
this.editor.saveDesign((design) => { ... });
await this.editor.getDesign();

// Export
await this.editor.exportHtml();
await this.editor.exportJson();
await this.editor.exportPlainText();
await this.editor.exportImage();
await this.editor.exportPdf();
await this.editor.exportZip();

// Configuration
this.editor.setMergeTags(config);
this.editor.setModules(modules);
this.editor.setFonts(config);
this.editor.setBodyValues(values);
this.editor.setAppearance(config);
this.editor.setLocale("fr-FR");
this.editor.setTextDirection("rtl");

// Actions
this.editor.undo();
this.editor.redo();
this.editor.save();
this.editor.showPreview("desktop");
this.editor.hidePreview();

// Raw SDK instance
const sdk = this.editor.getEditor();

// Events
const unsub = this.editor.addEventListener("row:selected", (data) => { ... });
unsub(); // unsubscribe
```

---

## Vanilla JavaScript

The SDK is loaded from CDN. There is no npm package.

### Basic Example

```html
<!DOCTYPE html>
<html>
<head>
  <title>Dragble Editor</title>
  <script src="https://sdk.dragble.com/latest/dragble-sdk.min.js"></script>
</head>
<body>
  <div id="editor-container" style="height: 800px;"></div>

  <script>
    // Global singleton
    dragble.init({
      containerId: "editor-container",
      editorKey: "ek_your_editor_key",
      editorMode: "email",
    });

    dragble.addEventListener("editor:ready", () => {
      console.log("Editor ready");
    });

    // Save
    document.getElementById("save-btn")?.addEventListener("click", async () => {
      const html = await dragble.exportHtml();
      const json = await dragble.exportJson();
      console.log("Exported:", { html, json });
    });
  </script>
</body>
</html>
```

### Multiple Instances with `createEditor()`

The global `dragble` object is a singleton. For multiple editors on the same page, use `createEditor()`:

```html
<div id="editor-1" style="height: 600px;"></div>
<div id="editor-2" style="height: 600px;"></div>

<script src="https://sdk.dragble.com/latest/dragble-sdk.min.js"></script>
<script>
  const editor1 = createEditor({
    containerId: "editor-1",
    editorKey: "ek_key_1",
    editorMode: "email",
  });

  const editor2 = createEditor({
    containerId: "editor-2",
    editorKey: "ek_key_2",
    editorMode: "web",
  });

  editor1.addEventListener("editor:ready", () => {
    console.log("Editor 1 ready");
  });

  editor2.addEventListener("editor:ready", () => {
    console.log("Editor 2 ready");
  });
</script>
```

**Important:** Do NOT use `dragble.init()` for multiple instances. Each `createEditor()` call returns an independent SDK instance with its own lifecycle.

### Loading a Saved Design

```javascript
dragble.init({
  containerId: "editor-container",
  editorKey: "ek_your_editor_key",
  design: savedDesignJson, // Pass design JSON at init time
});

// Or load after init:
dragble.addEventListener("editor:ready", () => {
  dragble.loadDesign(savedDesignJson);
});
```

### Cleanup

```javascript
// Destroy the editor instance and clean up resources
dragble.destroy();
```

---

## TypeScript Types

### Framework Wrappers (React, Vue, Angular)

Types are automatically available. All wrappers re-export the full type surface from `dragble-types`:

```typescript
import type {
  DragbleSDK,
  DragbleConfig,
  DragbleCallbacks,
  DesignJson,
  EditorOptions,
  EditorMode,
  MergeTag,
  MergeTagGroup,
  Module,
  PopupConfig,
  ExportHtmlOptions,
  ExportImageData,
  ExportPdfData,
  ExportZipData,
  CollaborationFeaturesConfig,
  UserInfo,
  CommentAction,
  // ... all types available
} from "dragble-react-editor"; // or "dragble-vue-editor" or "dragble-angular-editor"
```

### Vanilla TypeScript

Install the types package as a dev dependency:

```bash
npm install --save-dev dragble-types
```

Then declare the global SDK:

```typescript
import type { DragbleSDK } from "dragble-types";

declare global {
  const dragble: DragbleSDK;
  function createEditor(config: import("dragble-types").DragbleConfig): DragbleSDK;
}

// Now fully typed
dragble.init({
  containerId: "editor",
  editorKey: "ek_your_key",
});

const html: string = await dragble.exportHtml();
```

---

## SDK & Editor Versioning

### CDN URL Structure

```
https://sdk.dragble.com/{sdkVersion}/dragble-sdk.min.js
```

### Version Channels

| Channel | Example | Description |
|---|---|---|
| `latest` | `https://sdk.dragble.com/latest/dragble-sdk.min.js` | Latest release (default) |
| `stable` | `https://sdk.dragble.com/stable/dragble-sdk.min.js` | Stable release |
| `vX.Y.Z` | `https://sdk.dragble.com/v1.2.3/dragble-sdk.min.js` | Pinned version (note: `v` prefix required) |

### Wrapper Props

All framework wrappers accept four versioning props:

| Prop | Controls | Default | Description |
|---|---|---|---|
| `sdkVersion` | SDK script version | `"latest"` | Which SDK build to load from CDN |
| `sdkUrl` | SDK script URL | Derived from sdkVersion | Full URL override for the SDK script |
| `editorVersion` | Editor iframe version | `undefined` | Which editor build the SDK loads |
| `editorUrl` | Editor iframe URL | `undefined` | Full URL override for the editor |

### Precedence Rules

- `sdkUrl` overrides `sdkVersion`. If both are set, `sdkVersion` is ignored and a console warning is emitted.
- `editorUrl` overrides `editorVersion`. Same behavior.

### Examples

```tsx
// React: Pin SDK to v1.2.3, editor to stable
<DragbleEditor
  editorKey="ek_..."
  sdkVersion="v1.2.3"
  editorVersion="stable"
/>

// React: Use a self-hosted SDK
<DragbleEditor
  editorKey="ek_..."
  sdkUrl="https://cdn.example.com/dragble-sdk.min.js"
  editorUrl="https://editor.example.com"
/>
```

```vue
<!-- Vue -->
<DragbleEditor
  editor-key="ek_..."
  sdk-version="v1.2.3"
  editor-version="stable"
/>
```

```html
<!-- Vanilla: specific version -->
<script src="https://sdk.dragble.com/v1.2.3/dragble-sdk.min.js"></script>
```

---

## Events

Register event listeners with `addEventListener`. It returns an **unsubscribe function**.

```javascript
const unsubscribe = editor.addEventListener("design:updated", (data) => {
  console.log("Design updated:", data);
});

// Later: stop listening
unsubscribe();

// Or use removeEventListener with the same callback reference
editor.removeEventListener("design:updated", callback);
```

### All Events

| Event Name | Data Type | Description |
|---|---|---|
| `editor:ready` | `void` | Editor fully loaded and ready for interaction |
| `design:loaded` | `{ design: DesignJson }` | A design was loaded into the editor |
| `design:updated` | `{ design: DesignJson; type: string }` | The design changed (any edit) |
| `design:saved` | `{ design: DesignJson }` | Design was saved (via `save()`) |
| `row:selected` | `{ row: RowData }` | A row was selected |
| `row:unselected` | `void` | Row selection cleared |
| `column:selected` | `{ column: ColumnData }` | A column was selected |
| `column:unselected` | `void` | Column selection cleared |
| `content:selected` | `{ content: ContentData }` | A content block was selected |
| `content:unselected` | `void` | Content selection cleared |
| `content:modified` | `{ content: ContentData; changes: object }` | A content block was edited |
| `content:added` | `{ content: ContentData }` | A content block was added |
| `content:deleted` | `{ contentId: string }` | A content block was deleted |
| `preview:shown` | `{ device: ViewMode }` | Preview mode opened |
| `preview:hidden` | `void` | Preview mode closed |
| `image:uploaded` | `{ url: string }` | Image upload completed |
| `image:error` | `{ error: string }` | Image upload failed |
| `export:html` | `{ html: string }` | HTML export completed |
| `export:plainText` | `{ text: string }` | Plain text export completed |
| `export:image` | `ExportImageData` | Image export completed |
| `export` | `{ type: string; data: unknown }` | Generic export event |
| `save` | `void` | Save triggered |
| `save:success` | `{ design: DesignJson }` | Save completed successfully |
| `save:error` | `{ error: string }` | Save failed |
| `element:selected` | `{ type: string; id: string }` | Any element selected (generic) |
| `element:deselected` | `void` | Any element deselected (generic) |
| `template:requested` | `void` | User requested a template |
| `displayCondition:applied` | `{ contentId: string; condition: object }` | Display condition was set |
| `displayCondition:removed` | `{ contentId: string }` | Display condition was removed |
| `displayCondition:updated` | `{ contentId: string; condition: object }` | Display condition was changed |

---

## Callbacks

Callbacks are passed via the `callbacks` prop (framework wrappers) or the `callbacks` key in the init config (vanilla JS). They are divided into **non-blocking** and **blocking**.

### Non-Blocking Callbacks

These fire-and-forget. Return value is ignored.

| Callback | Signature | Description |
|---|---|---|
| `onReady` | `() => void` | Editor is fully loaded. In wrappers, use the dedicated `onReady` prop / `ready` emit instead. |
| `onLoad` | `(data: DesignData) => void` | Design was loaded. In wrappers, use the `onLoad` prop / `load` emit. |
| `onChange` | `(data: DesignData) => void` | Design changed. In wrappers, use the `onChange` prop / `change` emit. |
| `onError` | `(error: EditorError) => void` | Error occurred. In wrappers, use the `onError` prop / `error` emit. |
| `linkClick` | `(data: LinkClickData) => void` | User clicked a link in the editor canvas. |
| `onHeaderRowClick` | `(data: { rowId: string }) => void` | User clicked the locked header row. |
| `onFooterRowClick` | `(data: { rowId: string }) => void` | User clicked the locked footer row. |
| `onLockedRowClick` | `(data: { rowId: string }) => void` | User clicked any other locked row. |

### Blocking Callbacks (Return a Promise)

The editor **waits** for the Promise to resolve before continuing.

| Callback | Signature | Description |
|---|---|---|
| `onModuleSave` | `(data: ModuleSaveData) => Promise<ModuleSaveResult>` | User saves a row as a module. Receives row `data`, rendered `html`, generated `thumbnail`, `mode`, `type`, and normalized (lowercase) `category`. Return `{ success: true, moduleId? }` or `{ success: false, error? }`. |
| `onModuleDelete` | `(data: ModuleDeleteData) => Promise<{ success: boolean; error?: string }>` | User deletes a module from the Modules Library. Return `{ success: true }` to confirm or `{ success: false, error }` to keep it. |
| `onPreview` | `(html: string) => Promise<string>` | Preview opened. Receives raw HTML, must return (possibly transformed) HTML to display. |
| `onContentDialog` | `(info: ContentDialogInfo) => Promise<ContentDialogResult>` | Custom tool with `openOnDrop` requests a dialog. Return `{ values }` or `{ cancelled: true }`. |

### Example: Callbacks in React

```tsx
<DragbleEditor
  editorKey="ek_..."
  onReady={(editor) => console.log("Ready")}
  onChange={(data) => console.log("Changed:", data.type)}
  callbacks={{
    linkClick: (data) => window.open(data.url, "_blank"),
    onModuleSave: async (data) => {
      // data includes: id?, name, category, mode, type, data (row JSON), html, thumbnail?
      const result = await fetch("/api/modules", {
        method: "POST",
        body: JSON.stringify(data),
      });
      const { id } = await result.json();
      return { success: true, moduleId: id };
    },
    onModuleDelete: async (data) => {
      const response = await fetch(`/api/modules/${data.id}`, {
        method: "DELETE",
      });
      return response.ok
        ? { success: true }
        : { success: false, error: "Failed to delete module" };
    },
    onPreview: async (html) => {
      // Replace merge tags with sample data before preview
      return html.replace(/\{\{first_name\}\}/g, "Jane");
    },
  }}
/>
```

### Example: Callbacks in Vanilla JS

```javascript
dragble.init({
  containerId: "editor-container",
  editorKey: "ek_your_editor_key",
  callbacks: {
    onReady: () => console.log("Ready"),
    onChange: (data) => console.log("Changed"),
    linkClick: (data) => window.open(data.url, "_blank"),
    onModuleSave: async (data) => {
      const res = await fetch("/api/modules", {
        method: "POST",
        body: JSON.stringify(data),
      });
      const { id } = await res.json();
      return { success: true, moduleId: id };
    },
    onModuleDelete: async (data) => {
      const response = await fetch(`/api/modules/${data.id}`, {
        method: "DELETE",
      });
      return response.ok
        ? { success: true }
        : { success: false, error: "Failed to delete module" };
    },
    onPreview: async (html) => {
      return html.replace(/\{\{first_name\}\}/g, "Jane");
    },
  },
});
```

---

## Modules (Reusable Rows)

Modules let users save rows as reusable blocks that appear in the **Modules Library** accordion for drag-and-drop insertion.

### Module Data Structure

```ts
interface Module {
  id: string;                          // Unique identifier
  name: string;                        // Display name in the library
  category: string;                    // Category (normalized to lowercase by SDK)
  mode: "email" | "web" | "popup";    // Editor mode
  type: "standard" | "synced";        // standard = editable copy, synced = linked to source
  thumbnail?: string;                  // Optional preview image URL
  data: object;                        // Saved row JSON
}
```

Module categories are **canonicalized to lowercase** by the SDK/editor. `"Banner"`, `"banner"`, and `"BANNER"` all become `"banner"`. The UI displays them capitalized.

### Methods

```ts
// Replace the entire modules list
editor.setModules([
  {
    id: "hero-banner",
    name: "Hero Banner",
    category: "Banner",    // normalized to "banner"
    mode: "email",
    type: "standard",
    thumbnail: "https://cdn.example.com/thumbs/hero.png",
    data: heroRowJson,
  },
]);

// Append a single module
editor.addModule({
  id: "footer-standard",
  name: "Standard Footer",
  category: "footer",
  mode: "email",
  type: "standard",
  data: footerRowJson,
});

// Remove a module by ID
editor.removeModule("hero-banner");
```

### Synced Modules

Synced modules (`type: "synced"`) stay linked to their source. When you update a synced module, all designs using it reflect the changes on next load. Users **cannot edit** the content of a synced module inline — they must update the source module.

```ts
editor.setModules([
  {
    id: "legal-footer",
    name: "Legal Footer",
    category: "footer",
    mode: "email",
    type: "synced",  // Changes propagate to all designs
    data: legalFooterRowJson,
  },
]);
```

### Module Save Data (Returned by onModuleSave)

```ts
interface ModuleSaveData {
  id?: string;               // Existing module ID (if editing)
  name: string;              // User-provided name
  category: string;          // Normalized to lowercase
  mode: "email" | "web" | "popup";
  type: "standard" | "synced";
  data: object;              // Row JSON
  html: string;              // Rendered HTML
  thumbnail?: string;        // Auto-generated thumbnail URL
}
```

### Module Delete Data (Received by onModuleDelete)

```ts
interface ModuleDeleteData {
  id: string;
  name?: string;
  category?: string;
  type?: "standard" | "synced";
  mode?: "email" | "web" | "popup";
}
```

### Complete Callback Example

```tsx
<DragbleEditor
  editorKey="ek_..."
  callbacks={{
    onModuleSave: async (data) => {
      // data contains row data, html, thumbnail, mode, type, category
      const result = await fetch("/api/modules", {
        method: "POST",
        body: JSON.stringify(data),
      });
      const { id } = await result.json();
      return { success: true, moduleId: id };
    },
    onModuleDelete: async (data) => {
      const response = await fetch(`/api/modules/${data.id}`, {
        method: "DELETE",
      });
      return response.ok
        ? { success: true }
        : { success: false, error: "Failed to delete module" };
    },
  }}
/>
```

---

## Common Mistakes

### 1. Container too small or hidden

The editor renders inside an iframe. If the container has `height: 0`, `display: none`, or is inside a collapsed accordion, the editor will appear blank. Always ensure the container has a visible, non-zero height.

```tsx
// WRONG: no height
<DragbleEditor editorKey="ek_..." />

// RIGHT: explicit height
<DragbleEditor editorKey="ek_..." height="800px" />
```

### 2. Calling methods before the editor is ready

SDK methods throw or silently fail if called before `editor:ready`. Always wait for the ready event.

```javascript
// WRONG
dragble.init({ ... });
dragble.exportHtml(); // Editor not ready yet!

// RIGHT
dragble.addEventListener("editor:ready", async () => {
  const html = await dragble.exportHtml();
});
```

### 3. Only saving HTML (losing the design)

HTML export is for sending/rendering. To let users edit their design later, you must also save the **design JSON**.

```javascript
// WRONG: only save HTML
const html = await editor.exportHtml();
saveToDB(html);

// RIGHT: save both
const html = await editor.exportHtml();
const json = await editor.exportJson();
saveToDB({ html, json });

// Later: restore
editor.loadDesign(savedJson);
```

### 4. Using `dragble.init()` for multiple editors

The global `dragble` is a singleton. Calling `init()` twice replaces the first editor. Use `createEditor()` for multiple instances.

```javascript
// WRONG
dragble.init({ containerId: "editor-1", ... });
dragble.init({ containerId: "editor-2", ... }); // Replaces editor-1!

// RIGHT
const editor1 = createEditor({ containerId: "editor-1", ... });
const editor2 = createEditor({ containerId: "editor-2", ... });
```

Framework wrappers handle this automatically -- each `<DragbleEditor>` component creates its own instance via `createEditor()`.

### 5. Using wrong init parameters

Dragble uses `containerId` + `editorKey`. Do NOT use `id`, `projectId`, or `apiKey` -- those are from other editors.

```javascript
// WRONG (Unlayer-style)
dragble.init({ id: "editor", projectId: 12345 });

// RIGHT
dragble.init({ containerId: "editor", editorKey: "ek_your_key" });
```

### 6. Trying to `npm install @dragble/editor-sdk`

The SDK is CDN-only. There is no npm package for the SDK itself. Framework wrappers load it from CDN automatically.

### 7. Forgetting to destroy

In SPAs, always destroy the editor when the component unmounts. Framework wrappers handle this automatically, but in vanilla JS you must call `destroy()` manually.

```javascript
// On route change or cleanup
editor.destroy();
```

---

## Troubleshooting

### Blank editor (white iframe)

1. Check that the container element exists in the DOM when `init()` or the component mounts.
2. Ensure the container has a non-zero height (`height: 800px`, not `height: 0`).
3. Check the browser console for CSP (Content Security Policy) errors blocking the iframe or SDK script.
4. Verify the `editorKey` is valid and not expired.

### `editor:ready` never fires

1. Check the browser console for network errors loading the SDK from `sdk.dragble.com`.
2. Ensure the `editorKey` format is correct (`ek_*` or `px_*`).
3. Check that no ad blocker or firewall is blocking requests to `sdk.dragble.com` or the editor domain.
4. If using `sdkUrl` or `editorUrl`, verify the URLs are reachable and serve the correct files.

### Ref is undefined (React)

The ref is populated after the component mounts and the SDK loads. Use `onReady` instead of checking the ref immediately:

```tsx
// WRONG
useEffect(() => {
  editorRef.current?.editor?.exportHtml(); // ref not ready yet
}, []);

// RIGHT
<DragbleEditor
  ref={editorRef}
  onReady={(editor) => {
    // Safe to use editor here
  }}
/>
```

### Design doesn't load

1. Ensure the design JSON is a valid Dragble `DesignJson` object (not a string -- parse it first if stored as a string).
2. If passing `design` as a prop, it loads at init time. If calling `loadDesign()` manually, wait for `editor:ready`.
3. Check that the design was exported from the same editor mode (email designs won't load correctly in web mode).

### "Failed to load Dragble SDK" error

1. Check network connectivity to `sdk.dragble.com`.
2. If behind a corporate proxy, ensure the CDN domain is allowlisted.
3. If using a custom `sdkUrl`, verify the URL returns a valid JavaScript file.

### Editor re-initializes on every render (React)

In React, the editor re-initializes when `editorKey` or `containerId` changes. Ensure these are stable values:

```tsx
// WRONG: new key on every render
<DragbleEditor editorKey={`ek_${Date.now()}`} />

// RIGHT: stable key
<DragbleEditor editorKey="ek_your_stable_key" />
```

Callback props (`onReady`, `onChange`, etc.) are stored in refs internally and do NOT trigger re-initialization when they change.

### Multiple editors interfere with each other

Each editor instance must have a unique container element. Framework wrappers auto-generate unique container IDs. In vanilla JS, ensure each `containerId` is unique and use `createEditor()` (not `dragble.init()`).

