agentsclimarketplace

Dragble integration

Skill Dragble/dragble-skills/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

Install
npx -y skills add Dragble/dragble-skills --skill dragble-integration

Assembled 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

PackageInstallPurpose
dragble-react-editornpm install dragble-react-editorReact wrapper component
dragble-vue-editornpm install dragble-vue-editorVue 3 wrapper component
dragble-angular-editornpm install dragble-angular-editorAngular wrapper component
dragble-typesnpm install dragble-typesShared 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

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

PropTypeDefaultDescription
editorKeystringrequiredAuthentication key (ek_* or px_*)
editorMode"email" | "web" | "popup""email"Editor type
designDesignJson | ModuleData | nullundefinedInitial design to load
designMode"edit" | "live""live"Template permission mode
contentType"module"undefinedLock to single-row module editing
optionsEditorOptions{}All editor configuration (appearance, tools, features, AI, storage, etc.)
callbacksDragbleCallbacks (minus onReady/onLoad/onChange/onError)undefinedSDK callbacks (linkClick, onModuleSave, onPreview, etc.)
popupPopupConfigundefinedPopup configuration (when editorMode is "popup")
collaborationboolean | CollaborationFeaturesConfigfalseEnable collaboration/commenting
userUserInfoundefinedUser identity for sessions and collaboration
heightstring | numberundefinedEditor height (CSS value or pixels)
minHeightstring | number"600px"Minimum height
classNamestringundefinedCSS class on the outer wrapper
styleReact.CSSPropertiesundefinedInline styles on the outer wrapper
onReady(editor: DragbleSDK) => voidundefinedFires once when editor is fully loaded
onLoad() => voidundefinedFires when a design is loaded
onChange(data: { design: DesignJson; type: string }) => voidundefinedFires on every design change
onError(error: Error) => voidundefinedFires on SDK/loading errors
onComment(action: CommentAction) => voidundefinedFires on comment create/edit/delete/resolve/reopen
sdkUrlstringCDN latestCustom SDK script URL (overrides sdkVersion)
sdkVersionstring"latest"SDK version channel: "latest", "stable", or "vX.Y.Z"
editorVersionstringundefinedEditor version forwarded to SDK init config
editorUrlstringundefinedEditor 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)TypeDefaultDescription
editor-keystringrequiredAuthentication key
editor-mode"email" | "web" | "popup""email"Editor type
designDesignJson | ModuleData | nullundefinedInitial design
design-mode"edit" | "live"undefinedTemplate permission mode
content-type"module"undefinedSingle-row module editing
optionsEditorOptionsundefinedAll editor configuration
callbacksDragbleCallbacks (minus lifecycle)undefinedSDK callbacks
popupPopupConfigundefinedPopup config
collaborationboolean | CollaborationFeaturesConfigundefinedCollaboration features
userUserInfoundefinedUser identity
heightstring | number"600px"Editor height
min-heightstring | number"600px"Minimum height
merge-tags(MergeTag | MergeTagGroup)[]undefinedPersonalization tags
modulesModule[]undefinedCustom modules
display-conditionsDisplayConditionsConfigundefinedDisplay conditions
appearanceAppearanceConfigundefinedVisual customization
toolsToolsConfigundefinedTool enable/disable
featuresFeaturesConfigundefinedFeature toggles
fontsFontsConfigundefinedCustom fonts
localestringundefinedUI language locale
text-direction"ltr" | "rtl"undefinedText direction
body-valuesRecord<string, unknown>undefinedDefault body/canvas values
headerobjectundefinedLocked header row JSON
footerobjectundefinedLocked footer row JSON
aiAIConfigundefinedAI features configuration
custom-cssstring[]undefinedCustom CSS URLs/inline
custom-jsstring[]undefinedCustom JS URLs/inline
sdk-urlstringCDN latestCustom SDK script URL
sdk-versionstring"latest"SDK version channel
editor-versionstringundefinedEditor version
editor-urlstringundefinedEditor 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

EventPayloadDescription
readyDragbleSDKEditor is fully loaded
loadunknownDesign was loaded
change{ design: DesignJson; type: string }Design changed
errorErrorError occurred
commentCommentActionComment 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:

<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

InputTypeDefaultDescription
editorKeystringrequiredAuthentication key
editorModeEditorMode"email"Editor type
designDesignJson | ModuleData | nullundefinedInitial design
designMode"edit" | "live"undefinedTemplate permission mode
contentType"module"undefinedSingle-row module editing
optionsPartial<EditorOptions>undefinedAll editor configuration
callbacksDragbleCallbacks (minus lifecycle)undefinedSDK callbacks
popupPopupConfigundefinedPopup config
collaborationboolean | CollaborationFeaturesConfigundefinedCollaboration features
userUserInfoundefinedUser identity
heightstring | number"600px"Editor height
minHeightstring | number"600px"Minimum height
appearanceAppearanceConfigundefinedVisual customization
toolsToolsConfigundefinedTool enable/disable
featuresFeaturesConfigundefinedFeature toggles
fontsFontsConfigundefinedCustom fonts
mergeTagsMergeTagsConfigundefinedPersonalization tags
specialLinksSpecialLinksConfigundefinedSpecial link categories
modulesModule[]undefinedCustom modules
displayConditionsDisplayConditionsConfigundefinedDisplay conditions
localestringundefinedUI language locale
textDirectionTextDirectionundefinedText direction
languageLanguageundefinedMulti-language config
bodyValuesRecord<string, unknown>undefinedDefault body values
headerunknownundefinedLocked header row JSON
footerunknownundefinedLocked footer row JSON
aiAIConfigundefinedAI features configuration
customCSSstring[]undefinedCustom CSS
customJSstring[]undefinedCustom JS
sdkUrlstringCDN latestCustom SDK URL
sdkVersionstring"latest"SDK version channel
editorVersionstringundefinedEditor version
editorUrlstringundefinedEditor URL override

Outputs

OutputPayloadDescription
readyDragbleSDKEditor is fully loaded
loadunknownDesign was loaded
change{ design: DesignJson; type: string }Design changed
errorErrorError occurred
commentActionCommentActionComment 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:

@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

ChannelExampleDescription
latesthttps://sdk.dragble.com/latest/dragble-sdk.min.jsLatest release (default)
stablehttps://sdk.dragble.com/stable/dragble-sdk.min.jsStable release
vX.Y.Zhttps://sdk.dragble.com/v1.2.3/dragble-sdk.min.jsPinned version (note: v prefix required)

Wrapper Props

All framework wrappers accept four versioning props:

PropControlsDefaultDescription
sdkVersionSDK script version"latest"Which SDK build to load from CDN
sdkUrlSDK script URLDerived from sdkVersionFull URL override for the SDK script
editorVersionEditor iframe versionundefinedWhich editor build the SDK loads
editorUrlEditor iframe URLundefinedFull 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

// 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 NameData TypeDescription
editor:readyvoidEditor 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:unselectedvoidRow selection cleared
column:selected{ column: ColumnData }A column was selected
column:unselectedvoidColumn selection cleared
content:selected{ content: ContentData }A content block was selected
content:unselectedvoidContent 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:hiddenvoidPreview 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:imageExportImageDataImage export completed
export{ type: string; data: unknown }Generic export event
savevoidSave 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:deselectedvoidAny element deselected (generic)
template:requestedvoidUser 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.

CallbackSignatureDescription
onReady() => voidEditor is fully loaded. In wrappers, use the dedicated onReady prop / ready emit instead.
onLoad(data: DesignData) => voidDesign was loaded. In wrappers, use the onLoad prop / load emit.
onChange(data: DesignData) => voidDesign changed. In wrappers, use the onChange prop / change emit.
onError(error: EditorError) => voidError occurred. In wrappers, use the onError prop / error emit.
linkClick(data: LinkClickData) => voidUser clicked a link in the editor canvas.
onHeaderRowClick(data: { rowId: string }) => voidUser clicked the locked header row.
onFooterRowClick(data: { rowId: string }) => voidUser clicked the locked footer row.
onLockedRowClick(data: { rowId: string }) => voidUser clicked any other locked row.

Blocking Callbacks (Return a Promise)

The editor waits for the Promise to resolve before continuing.

CallbackSignatureDescription
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)

  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:

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

// 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.

Keep looking

Skills are one crate of 326,629. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.