An open API service indexing awesome lists of open source software.

https://github.com/Dragble/dragble-angular-editor

AI Powered Embeddable email editor for React — build responsive HTML email templates, newsletters, and marketing campaigns with drag-and-drop
https://github.com/Dragble/dragble-angular-editor

builder email email-builder email-editor email-marketing email-template html-email

Last synced: 2 days ago
JSON representation

AI Powered Embeddable email editor for React — build responsive HTML email templates, newsletters, and marketing campaigns with drag-and-drop

Awesome Lists containing this project

README

          



Dragble - AI-Powered Angular Email Template Builder


npm version
license

# dragble-angular-editor

The **fully AI-powered** Angular editor for **email templates** and **landing pages**. Your end-users design visually with drag-and-drop — or describe what they want and watch AI agents build it live on the canvas. Powered by the built-in **Model Context Protocol (MCP)** server, connect [Claude Code](https://claude.com/code), [OpenCode](https://opencode.ai), [Codex](https://github.com/openai/codex), [Cursor](https://cursor.com), or your own AI backend directly to the editor. Structured tool calls mean guaranteed-valid output — no prompt engineering, no JSON hallucination, no broken layouts.

[Dragble](https://dragble.com) brings two design experiences together in one Angular component: a polished visual editor for designers and a conversational AI surface for everyone else — backed by structured tool calls that produce guaranteed-valid HTML emails and landing pages every time.

[Website](https://dragble.com) | [Documentation](https://docs.dragble.com) | [Dashboard](https://developers.dragble.com)


Dragble - AI-Powered Angular Email Editor with Drag and Drop

## Features

- Drag-and-drop **email template builder** with 20+ content blocks
- **Fully AI-powered via MCP** — connect AI agents (Claude Code, OpenCode, Codex, Cursor) or your own AI backend to build designs live on the canvas. Structured tool calls mean guaranteed-valid output — no prompt engineering, no JSON hallucination
- Responsive **HTML email** output compatible with all major email clients
- **Newsletter editor** with merge tags, dynamic content, and display conditions
- Visual **email designer** — no HTML/CSS knowledge required for end users
- Export to HTML, JSON, image, PDF, or ZIP
- Built-in image editor, AI content generation, and collaboration tools
- Full TypeScript support
- Works with NgModule and standalone components (Angular 14+)

## Installation

The SDK is loaded from CDN automatically — you only need to install the Angular package.

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

```bash
yarn add dragble-angular-editor
```

```bash
pnpm add dragble-angular-editor
```

## Editor Key

An `editorKey` is required to use the editor. You can get one by creating a project on the [Dragble Developer Dashboard](https://developers.dragble.com).

## Usage

### NgModule

```typescript
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { DragbleEditorModule } from "dragble-angular-editor";
import { AppComponent } from "./app.component";

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

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

@Component({
selector: "app-email-editor",
template: `


Save
Export HTML


`,
})
export class EmailEditorComponent {
@ViewChild("editor") editor!: DragbleEditorComponent;

onReady(sdk: DragbleSDK): void {
console.log("Editor ready!", sdk);
}

async onChange(data: { design: DesignJson; type: string }): Promise {
// Design JSON is available directly from the callback
console.log("Design JSON:", data.design);

// To get HTML, call exportHtml on the editor
const html = await this.editor.exportHtml();
console.log("HTML:", html);
}

async handleSave(): Promise {
const design = await this.editor.getDesign();
console.log("Design:", design);
}

async handleExport(): Promise {
const html = await this.editor.exportHtml();
console.log("HTML:", html);
}
}
```

### Standalone Component (Angular 17+)

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

@Component({
selector: "app-editor",
standalone: true,
imports: [DragbleEditorComponent],
template: `

`,
})
export class EditorComponent {
@ViewChild("editor") editor!: DragbleEditorComponent;

onReady(): void {
console.log("Editor ready!");
}
}
```

## Complete Example

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

@Component({
selector: "app-advanced-email-builder",
standalone: true,
imports: [DragbleEditorComponent],
styles: [
`
.advanced-email-builder {
height: 100vh;
display: flex;
flex-direction: column;
}

.toolbar {
padding: 12px;
border-bottom: 1px solid #ddd;
display: flex;
gap: 8px;
align-items: center;
}

.dirty-indicator {
color: orange;
}
`,
],
template: `



Undo
Redo

Preview

Export HTML

Export Image

Unsaved changes



`,
})
export class AdvancedEmailBuilderComponent {
@ViewChild("editor") editor!: DragbleEditorComponent;

isDirty = false;

editorOptions: EditorOptions = {
appearance: { theme: "light" },
features: {
preview: true,
undoRedo: true,
imageEditor: true,
},
};

handleReady(editor: DragbleSDK): void {
// Set merge tags (must pass a MergeTagsConfig object)
editor.setMergeTags({
customMergeTags: [
{ name: "First Name", value: "{{first_name}}" },
{ name: "Last Name", value: "{{last_name}}" },
{ name: "Company", value: "{{company}}" },
],
excludeDefaults: false,
sort: true,
});

// Set custom fonts
editor.setFonts({
showDefaultFonts: true,
customFonts: [{ label: "Brand Font", value: "BrandFont, sans-serif" }],
});

// Load saved design if available
const savedDesign = localStorage.getItem("email-design");
if (savedDesign) {
editor.loadDesign(JSON.parse(savedDesign));
}
}

handleChange(data: { design: DesignJson; type: string }): void {
this.isDirty = true;
localStorage.setItem("email-design", JSON.stringify(data.design));
}

async handleExportHtml(): Promise {
const html = await this.editor.exportHtml();
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "email.html";
a.click();
URL.revokeObjectURL(url);
}

async handleExportImage(): Promise {
const data = await this.editor.exportImage();
window.open(data.url, "_blank");
}

handleError(error: Error): void {
console.error(error.message);
}
}
```

## MCP — AI Integration

Connect AI agents (Claude Code, OpenCode, Codex, Cursor, or your own AI backend) to the editor through the [Model Context Protocol](https://modelcontextprotocol.io). The AI calls structured tools — `add_row`, `add_heading`, `update_button`, `export_html` — that mutate design state live on the canvas. No prompt engineering, no JSON hallucination, no broken output.

### Enabling MCP

MCP is off by default. Set `features: { mcp: true }` to opt in:

```html

```

MCP also requires a **Starter plan or higher**. Both conditions must be true — plan allows it AND SDK enables it.

### Quick example — your backend controls the AI

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

@Component({
selector: "app-editor",
template: `
Connect AI

`,
})
export class EditorComponent {
@ViewChild("editor") editor!: DragbleEditorComponent;

async handleConnectAI() {
// The id is YOUR identifier — derive it from your own database/session
// so the same user editing the same document always gets the same MCP
// session. Example: if your logged-in user is "alice123" and they're
// editing document "campaign-summer-2026", build an id like this:
//
// const id = "alice123-campaign-summer-2026";
//
// Format rules: 8-128 chars, only letters/digits/hyphens/underscores.
const userIdFromAuth = "alice123"; // from your auth/session
const docIdFromRoute = "campaign-summer"; // from your URL or DB row
const id = `${userIdFromAuth}-${docIdFromRoute}`;
const { sessionId } = await this.editor.editor!.connectMCP({ id });
// Pass sessionId to your backend — it calls MCP tools with your mcp_key
}
}
```

### Quick example — end-user pairs their own AI client

```typescript
const handleLetUserPair = async () => {
const editor = this.editor.editor!;
// Same id you'd use anywhere else for this user+document combination.
// 8-128 chars, only letters/digits/hyphens/underscores.
const id = "alice123-campaign-summer-2026";
await editor.connectMCP({ id });

// Explicitly generate a pairing code (not auto-generated)
const { code, expiresAt } = await editor.getPairingCode();
alert(`Paste this into Claude Code: ${code}`);
};
```

### One controller per session

Each session can be controlled by **either** your backend **or** an end-user's AI client (Claude Code, OpenCode), never both at the same time:

- If your backend makes the first tool call → session is locked to **backend**. Pairing codes are rejected.
- If a user pairs via pairing code first → session is locked to **paired client**. Backend tool calls are rejected.

This prevents two AI controllers from conflicting on the same design.

### How it works

1. **Enable MCP** in the SDK config: `features: { mcp: true }`.
2. **Generate an MCP key** in the Dragble dashboard: Project → MCP Key → Generate. Store it in your backend env vars — never in browser code.
3. **Call `editor.connectMCP({ id })`** where `id` is a stable identifier you control (see below).
4. **Choose your AI path**: either your backend calls MCP tools directly (using the mcp_key), or you generate a pairing code for the end-user to connect their own AI client.
5. **Mutations stream live** onto the editor canvas as the AI works.

### The `id` parameter — why it matters

The `id` you pass to `connectMCP()` is a **Bring Your Own ID (BYOI)** that maps to your domain entities. It is NOT a random token — it is how Dragble identifies the session across browser refreshes, server restarts, and device switches.

**Rules:**

- 8–128 characters long
- Only letters, numbers, hyphens, and underscores (`a-z A-Z 0-9 - _`)
- Must be deterministic — the same user editing the same document should always produce the same `id`

**Why these rules?**

- The `id` is used in database lookups and URL paths — special characters or extreme lengths would break routing
- Same `id` = resume the same session. Random UUIDs mean every page refresh creates a new session and loses AI context
- Short IDs (< 8 chars) are too easy to guess, long IDs (> 128 chars) waste storage

```typescript
// Recommended: derive from your domain — concrete examples
editor.connectMCP({ id: "alice123-campaign-summer-2026" }); // user + doc
editor.connectMCP({ id: "workspace_acme_template_welcome" }); // workspace + template
editor.connectMCP({ id: "org-uber-eats-promo-q4-2026" }); // org + campaign
editor.connectMCP({ id: "tenant_42_invoice_template_v3" }); // tenant + entity

// Valid but NOT recommended — random IDs break session continuity
// (every page refresh creates a brand new session, AI loses context)
editor.connectMCP({ id: crypto.randomUUID() });
```

### Disconnecting

`disconnectMCP()` permanently destroys the session — the session cannot be reopened:

```typescript
const { destroyed } = await editor.disconnectMCP();
```

Your backend can also force-destroy a session server-side (e.g., when a user's subscription ends):

```bash
curl -X DELETE https://mcp.dragble.com/sessions/user-42-doc-99 \
-H "X-API-Key: db_mcp_your_key_here"
```

Idle sessions are reaped after 2 hours of inactivity. Active sessions never expire — each tool call resets the timer.

### MCP method reference

| Method | Returns |
| -------------------------------------------------- | ----------------------------------------------------------------------- |
| `editor.connectMCP({ id, editorMode? })` | `{ sessionId, resumed? }` |
| `editor.disconnectMCP()` | `{ destroyed }` — permanently deletes session |
| `editor.getPairingCode()` | `{ code, expiresAt }` — generate a pairing code for end-user AI clients |
| `editor.endPairing()` | `{ revoked }` — invalidate the active pairing code |
| `editor.getMCPStatus()` | `{ paired: true, sessionId } \| { paired: false, reason? }` |
| `editor.onAIToolFired(cb)` | unsubscribe fn — fires when AI calls any tool |

### Full documentation

- [MCP Overview](https://docs.dragble.com/mcp-server/overview)
- [Credentials & Security](https://docs.dragble.com/mcp-server/credentials)
- [AI Client Setup (OpenCode, Claude Code, Codex, etc.)](https://docs.dragble.com/mcp-server/ai-client-setup)

## Inputs

| Input | Type | Default | Description |
| ------------------- | ---------------------------------------- | ------------ | ------------------------------------- |
| `editorKey` | `string` | **required** | Editor key for authentication |
| `design` | `DesignJson \| ModuleData \| null` | `undefined` | Initial design to load |
| `editorMode` | `EditorMode` | `"email"` | `"email"`, `"web"`, or `"popup"` |
| `popup` | `PopupConfig` | `undefined` | Popup configuration |
| `contentType` | `EditorContentTypeValue` | `undefined` | Set to `"module"` for single-row mode |
| `ai` | `AIConfig` | `undefined` | AI features configuration |
| `locale` | `string` | `undefined` | UI locale |
| `translations` | `Record>` | `undefined` | Translation overrides |
| `textDirection` | `TextDirection` | `undefined` | `"ltr"` or `"rtl"` |
| `language` | `Language` | `undefined` | Template language |
| `appearance` | `AppearanceConfig` | `undefined` | Visual customization |
| `tools` | `ToolsConfig` | `undefined` | Tool enable/disable |
| `customTools` | `DragbleToolConfig[]` | `undefined` | Custom tool definitions |
| `features` | `FeaturesConfig` | `undefined` | Feature toggles |
| `fonts` | `FontsConfig` | `undefined` | Fonts configuration |
| `bodyValues` | `Record` | `undefined` | Body-level values |
| `header` | `unknown` | `undefined` | Locked header row |
| `footer` | `unknown` | `undefined` | Locked footer row |
| `mergeTags` | `MergeTagsConfig` | `undefined` | Merge tags configuration |
| `specialLinks` | `SpecialLinksConfig` | `undefined` | Special links configuration |
| `modules` | `Module[]` | `undefined` | Custom modules |
| `displayConditions` | `DisplayConditionsConfig` | `undefined` | Display conditions |
| `editor` | `EditorBehaviorConfig` | `undefined` | Editor behavior settings |
| `customCSS` | `string[]` | `undefined` | Custom CSS URLs |
| `customJS` | `string[]` | `undefined` | Custom JS URLs |
| `height` | `string \| number` | `"600px"` | Editor height |
| `minHeight` | `string \| number` | `"600px"` | Minimum height |
| `options` | `Partial` | `undefined` | Additional editor options |
| `callbacks` | `Omit` | `undefined` | SDK callbacks |

| `collaboration` | `boolean \| CollaborationFeaturesConfig` | `undefined` | Collaboration settings |
| `user` | `UserInfo` | `undefined` | Current user info |
| `designMode` | `"edit" \| "live"` | `undefined` | Template permissions mode |

## Outputs

| Output | Payload Type | Description |
| --------------- | -------------------------------------- | -------------------------------- |
| `ready` | `DragbleSDK` | Emitted when the editor is ready |
| `load` | `unknown` | Emitted when a design is loaded |
| `change` | `{ design: DesignJson; type: string }` | Emitted on every design change |
| `error` | `Error` | Emitted when an error occurs |
| `commentAction` | `CommentAction` | Emitted on comment events |

## SDK Methods Reference

Access methods through `@ViewChild` on the Angular component. The wrapper exposes SDK methods directly, so call `this.editor.exportHtml()` from your component class. All export and getter methods return Promises.

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

const html = await this.editor.exportHtml();
```

### Design

```typescript
this.editor.loadDesign(design, options?); // void
const result = await this.editor.loadDesignAsync(design, options?);
// => { success, validRowsCount, invalidRowsCount, errors? }
this.editor.loadBlank(options?); // void
const { html, json } = await this.editor.getDesign(); // Promise
```

### Export

All export methods are **Promise-based**. There are no callback overloads.

```typescript
const html = await this.editor.exportHtml(options?); // Promise
const json = await this.editor.exportJson(); // Promise
const text = await this.editor.exportPlainText(); // Promise
const imageData = await this.editor.exportImage(options?); // Promise
const pdfData = await this.editor.exportPdf(options?); // Promise
const zipData = await this.editor.exportZip(options?); // Promise
const values = await this.editor.getPopupValues(); // Promise
```

### Merge Tags

`setMergeTags` accepts a `MergeTagsConfig` object, not a plain array.

```typescript
this.editor.setMergeTags({
customMergeTags: [
{ name: "First Name", value: "{{first_name}}" },
{ name: "Company", value: "{{company}}" },
],
excludeDefaults: false,
sort: true,
});
const tags = await this.editor.getMergeTags(); // Promise<(MergeTag | MergeTagGroup)[]>
```

### Special Links

`setSpecialLinks` accepts a `SpecialLinksConfig` object.

```typescript
this.editor.setSpecialLinks({
customSpecialLinks: [{ name: "Unsubscribe", href: "{{unsubscribe_url}}" }],
excludeDefaults: false,
});
const links = await this.editor.getSpecialLinks(); // Promise<(SpecialLink | SpecialLinkGroup)[]>
```

### Modules

```typescript
this.editor.setModules(modules); // void
this.editor.setModulesLoading(loading); // void
const modules = await this.editor.getModules(); // Promise
```

### Fonts

```typescript
this.editor.setFonts(config); // void
const fonts = await this.editor.getFonts(); // Promise
```

### Body Values

```typescript
this.editor.setBodyValues({
backgroundColor: "#f5f5f5",
contentWidth: "600px",
});
const values = await this.editor.getBodyValues(); // Promise
```

### Editor Configuration

```typescript
this.editor.setOptions(options); // void — Partial
this.editor.setToolsConfig(toolsConfig); // void
this.editor.setEditorMode(mode); // void
this.editor.setEditorConfig(config); // void
const config = await this.editor.getEditorConfig(); // Promise
```

### Locale, Language & Text Direction

```typescript
this.editor.setLocale(locale, translations?); // void
this.editor.setLanguage(language); // void
const lang = await this.editor.getLanguage(); // Promise
this.editor.setTextDirection(direction); // void — 'ltr' | 'rtl'
const dir = await this.editor.getTextDirection(); // Promise
```

### Appearance

```typescript
this.editor.setAppearance(appearance); // void
```

### Undo / Redo / Save

```typescript
this.editor.undo(); // void
this.editor.redo(); // void
const canUndo = await this.editor.canUndo(); // Promise
const canRedo = await this.editor.canRedo(); // Promise
this.editor.save(); // void
```

### Preview

```typescript
this.editor.showPreview(device?); // void — 'desktop' | 'tablet' | 'mobile'
this.editor.hidePreview(); // void
```

### Custom Tools

```typescript
await this.editor.registerTool(config); // Promise
await this.editor.unregisterTool(toolId); // Promise
const tools = await this.editor.getTools(); // Promise>
```

### Custom Widgets

```typescript
await this.editor.createWidget(config); // Promise
await this.editor.removeWidget(widgetName); // Promise
```

### Collaboration & Comments

```typescript
this.editor.showComment(commentId); // void
this.editor.openCommentPanel(rowId); // void
```

### Tabs & Branding

```typescript
this.editor.updateTabs(tabs); // void
this.editor.setBrandingColors(config); // void
this.editor.registerColumns(cells); // void
```

### Display Conditions

```typescript
this.editor.setDisplayConditions(config); // void
```

### Audit

```typescript
const result = await this.editor.audit(options?); // Promise
```

### Asset Management

```typescript
const { success, url, error } = await this.editor.uploadImage(file, options?);
const { assets, total } = await this.editor.listAssets(options?);
const { success, error } = await this.editor.deleteAsset(assetId);
const folders = await this.editor.listAssetFolders(parentId?);
const folder = await this.editor.createAssetFolder(name, parentId?);
const info = await this.editor.getStorageInfo();
```

### Status & Lifecycle

```typescript
this.editor.isReady(); // boolean
this.editor.destroy(); // void
```

## Events

Angular outputs (`(ready)`, `(load)`, `(change)`, `(error)`, `(commentAction)`) cover the common component integration points. For lower-level SDK events, subscribe with `addEventListener` after the editor is ready:

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

// Or remove manually
this.editor.removeEventListener("design:updated", callback);
```

### Available Events

| Event | Description |
| -------------------------- | --------------------------- |
| `editor:ready` | Editor initialized |
| `design:loaded` | Design loaded |
| `design:updated` | Design changed |
| `design:saved` | Design saved |
| `row:selected` | Row selected |
| `row:unselected` | Row unselected |
| `column:selected` | Column selected |
| `column:unselected` | Column unselected |
| `content:selected` | Content block selected |
| `content:unselected` | Content block unselected |
| `content:modified` | Content block modified |
| `content:added` | Content block added |
| `content:deleted` | Content block deleted |
| `preview:shown` | Preview opened |
| `preview:hidden` | Preview closed |
| `image:uploaded` | Image uploaded successfully |
| `image:error` | Image upload error |
| `export:html` | HTML exported |
| `export:plainText` | Plain text exported |
| `export:image` | Image exported |
| `save` | Save triggered |
| `save:success` | Save succeeded |
| `save:error` | Save failed |
| `template:requested` | Template requested |
| `element:selected` | Element selected |
| `element:deselected` | Element deselected |
| `export` | Export triggered |
| `displayCondition:applied` | Display condition applied |
| `displayCondition:removed` | Display condition removed |
| `displayCondition:updated` | Display condition updated |

## TypeScript

All types are exported from `dragble-angular-editor`:

```typescript
import type {
DesignJson,
EditorMode,
DragbleSDK,
MergeTagsConfig,
AppearanceConfig,
FeaturesConfig,
ToolsConfig,
FontsConfig,
} from "dragble-angular-editor";
```

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to contribute to this project.

## License

[MIT](./LICENSE)