Skip to main content
Version: Latest

Callbacks

Callbacks let you hook into editor lifecycle events and intercept user actions. Pass them via the callbacks property in your configuration.

Configuration​

const config: DragbleConfig = {
callbacks: {
onReady: () => {
/* ... */
},
onLoad: (design) => {
/* ... */
},
onChange: (design) => {
/* ... */
},
onError: (error) => {
/* ... */
},
onModuleSave: async (data) => {
/* ... */
},
onModuleDelete: async (data) => {
/* ... */
return { success: true };
},
onPreview: (data) => {
/* ... */
},
onContentDialog: async (data) => {
/* ... */
},
onHeaderRowClick: () => {
/* ... */
},
onFooterRowClick: () => {
/* ... */
},
onLockedRowClick: (data) => {
/* ... */
},
},
};

Callback Reference​

onReady​

Type: () => void Blocking: No

Fires when the editor is fully initialized and ready for interaction.

onReady: () => {
console.log('Editor is ready');
enableSaveButton();
},

onLoad​

Type: (design: DesignJson) => void Blocking: No

Fires when a design is loaded into the editor (via loadDesign() or the initial design prop).

onLoad: (design) => {
console.log('Design loaded', design.body.rows.length, 'rows');
},

onChange​

Type: (design: DesignJson) => void Blocking: No

Fires on every design modification. Use for auto-save or dirty-state tracking.

onChange: (design) => {
setIsDirty(true);
debouncedSave(design);
},
warning

onChange fires frequently. Debounce any expensive operations (network requests, large state updates) to avoid performance issues.

onError​

Type: (error: EditorError) => void Blocking: No

Fires when the editor encounters an error.

onError: (error) => {
console.error('Editor error:', error.message, error.code);
reportToSentry(error);
},

onModuleSave​

Type: (data: ModuleSaveData) => Promise<ModuleSaveResult> Blocking: Yes

Fires when the user saves a reusable module (content block). The editor waits for the returned promise to resolve before completing the save.

interface ModuleSaveData {
name: string;
category: string; // normalized lowercase category
type: "standard" | "synced";
mode: "email" | "web";
data: unknown; // saved row JSON
html: string;
thumbnail?: string; // generated PNG data URL or hosted URL
schemaVersion?: number;
}

Category casing is normalized before the callback fires. If the user selected "Banner", the callback receives category: "banner".

onModuleSave: async (data) => {
await fetch('/api/modules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
},

onModuleDelete​

Type: (data: ModuleDeleteData) => Promise<ModuleDeleteResult> Blocking: Yes

Fires when the user deletes a module from the Modules Library. The editor waits for the returned promise and removes the module locally only when success is true.

The editor sends this callback request through the SDK bridge:

{
type: "DRAGBLE_CALLBACK_RESPONSE",
callbackType: "onModuleDelete",
requestId,
data: {
id: module.id,
name: module.name,
category: module.category,
type: module.type,
mode: module.mode,
},
}

Callback data shape:

interface ModuleDeleteData {
id: string;
name?: string;
category?: string; // normalized lowercase category
type?: "standard" | "synced";
mode?: "email" | "web";
}

Return { success: true } after deleting the module in your backend. Return { success: false, error } to keep the module visible in the editor.

interface ModuleDeleteResult {
success: boolean;
error?: string;
}

onModuleDelete: async (data) => {
const response = await fetch(`/api/modules/${data.id}`, {
method: "DELETE",
});

if (!response.ok) {
return { success: false, error: "Failed to delete module" };
}

return { success: true };
},

onPreview​

Type: (data: PreviewData) => void Blocking: No

Fires when the user clicks the preview button. Use to open a custom preview instead of the built-in modal.

interface PreviewData {
html: string;
design: DesignJson;
}
onPreview: (data) => {
openCustomPreviewModal(data.html);
},

onContentDialog​

Type: (data: ContentDialogData) => Promise<DesignJson> Blocking: Yes

Fires when the user opens a content dialog (e.g., template picker, saved content browser). Return a DesignJson to insert into the editor.

interface ContentDialogData {
type: "template" | "module" | "content";
}
onContentDialog: async (data) => {
const selectedTemplate = await openTemplatePicker(data.type);
return selectedTemplate.design;
},

onHeaderRowClick​

Type: () => void Blocking: No

Fires when the user clicks the locked header row. Use to show a settings dialog for the header.

onHeaderRowClick: () => {
openHeaderSettings();
},

onFooterRowClick​

Type: () => void Blocking: No

Fires when the user clicks the locked footer row.

onFooterRowClick: () => {
openFooterSettings();
},

onLockedRowClick​

Type: (data: { rowId: string }) => void Blocking: No

Fires when the user clicks any locked row (other than header/footer).

onLockedRowClick: (data) => {
console.log('Locked row clicked:', data.rowId);
openRowSettings(data.rowId);
},

Blocking vs Fire-and-Forget​

CallbackBlocking
onReadyNo
onLoadNo
onChangeNo
onErrorNo
onModuleSaveYes — editor waits for the promise
onModuleDeleteYes — editor waits for the promise
onPreviewNo
onContentDialogYes — editor waits for the returned design
onHeaderRowClickNo
onFooterRowClickNo
onLockedRowClickNo
info

Blocking callbacks must return a Promise. The editor shows a loading indicator while waiting. If the promise rejects, the operation is cancelled and a toast notification is shown.

tip

For AI-related callbacks (onSmartTextGenerate, onImageGenerate, etc.), see the AI section.