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.From its SKILL.md
npx -y skills add Dragble/dragble-skills --skill dragble-integrationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
37.4 KB, ~9.7k tokens by cl100k_base, as published. Nobody here has run it
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_*orpx_*. - 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
npm install dragble-react-editor
Types are included automatically via re-export from dragble-types.
Complete Example
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)
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
import { useDragbleEditor } from "dragble-react-editor";
function MyComponent() {
const { ref, editor, isReady } = useDragbleEditor();
return (
<DragbleEditor ref={ref} editorKey="ek_..." />
);
}
Vue 3
Installation
npm install dragble-vue-editor
Types are included automatically via re-export from dragble-types.
Complete Example
<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-- callsloadDesign()when changedmerge-tags-- callssetMergeTags()when changedmodules-- callssetModules()when changeddisplay-conditions-- callssetDisplayConditions()when changed
Component Ref Methods
All SDK methods are exposed via expose() on the component ref:
<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:
<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
npm install dragble-angular-editor
Types are included automatically via re-export from dragble-types.
Complete Example (Standalone Component)
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:
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-- callsloadDesign()mergeTags-- callssetMergeTags()modules-- callssetModules()displayConditions-- callssetDisplayConditions()
Public Methods
All SDK methods are exposed as public methods on the component:
@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
<!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():
<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
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
// 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:
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:
npm install --save-dev dragble-types
Then declare the global SDK:
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
sdkUrloverridessdkVersion. If both are set,sdkVersionis ignored and a console warning is emitted.editorUrloverrideseditorVersion. Same behavior.
Examples
// 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 -->
<DragbleEditor
editor-key="ek_..."
sdk-version="v1.2.3"
editor-version="stable"
/>
<!-- 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.
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
<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
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
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
// 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.
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)
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)
interface ModuleDeleteData {
id: string;
name?: string;
category?: string;
type?: "standard" | "synced";
mode?: "email" | "web" | "popup";
}
Complete Callback Example
<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.
// 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.
// 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.
// 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.
// 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.
// 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.
// On route change or cleanup
editor.destroy();
Troubleshooting
Blank editor (white iframe)
- Check that the container element exists in the DOM when
init()or the component mounts. - Ensure the container has a non-zero height (
height: 800px, notheight: 0). - Check the browser console for CSP (Content Security Policy) errors blocking the iframe or SDK script.
- Verify the
editorKeyis valid and not expired.
editor:ready never fires
- Check the browser console for network errors loading the SDK from
sdk.dragble.com. - Ensure the
editorKeyformat is correct (ek_*orpx_*). - Check that no ad blocker or firewall is blocking requests to
sdk.dragble.comor the editor domain. - If using
sdkUrloreditorUrl, 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:
// 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
- Ensure the design JSON is a valid Dragble
DesignJsonobject (not a string -- parse it first if stored as a string). - If passing
designas a prop, it loads at init time. If callingloadDesign()manually, wait foreditor:ready. - 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
- Check network connectivity to
sdk.dragble.com. - If behind a corporate proxy, ensure the CDN domain is allowlisted.
- 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:
// 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()).
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.