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);
},
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​
| Callback | Blocking |
|---|---|
onReady | No |
onLoad | No |
onChange | No |
onError | No |
onModuleSave | Yes — editor waits for the promise |
onModuleDelete | Yes — editor waits for the promise |
onPreview | No |
onContentDialog | Yes — editor waits for the returned design |
onHeaderRowClick | No |
onFooterRowClick | No |
onLockedRowClick | No |
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.
For AI-related callbacks (onSmartTextGenerate, onImageGenerate, etc.), see the AI section.