Skip to content

Events

The editor communicates with your application through callback functions passed in the configuration.

Content Events

onChange

Called whenever the template content changes. The callback receives the full TemplateContent object. Changes are debounced internally.

ts
const editor = init({
  container: '#editor',
  onChange(content) {
    // Save to your backend
    fetch('/api/templates/123', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(content),
    });
  },
});

onSave

Called when the user explicitly triggers a save (e.g. via keyboard shortcut). Use this for immediate saves vs. the debounced onChange.

ts
const editor = init({
  container: '#editor',
  onSave(content) {
    saveTemplate(content);
    showNotification('Template saved');
  },
});

onError

Called when an error occurs within the editor.

ts
const editor = init({
  container: '#editor',
  onError(error) {
    console.error('Editor error:', error.message);
    reportToSentry(error);
  },
});

Request Events

onRequestMedia

Called when the user clicks to select an image (e.g. in the image block settings). The editor passes a callback function — call it with the image URL when the user has made their selection.

ts
const editor = init({
  container: '#editor',
  onRequestMedia(callback) {
    // Open your own media picker
    openMediaPicker({
      onSelect(imageUrl) {
        callback(imageUrl);
      },
    });
  },
});

If you don't provide onRequestMedia, the editor falls back to a text input where users type or paste image URLs directly.

onRequestMergeTag

Called when the user clicks to insert a merge tag in a text block. Return a Promise that resolves to a MergeTag object or null if the user cancels.

ts
import type { MergeTag } from '@templatical/types';

const editor = init({
  container: '#editor',
  mergeTags: {
    syntax: 'liquid',
    tags: [
      { label: 'First Name', value: '{{first_name}}' },
      { label: 'Email', value: '{{email}}' },
    ],
  },
  async onRequestMergeTag(): Promise<MergeTag | null> {
    // Show your own picker UI and return the selected tag
    const tag = await showMergeTagPicker();
    return tag; // or null if cancelled
  },
});

If you provide mergeTags.tags without onRequestMergeTag, the editor uses a built-in dropdown populated with your tags. The onRequestMergeTag callback lets you replace that dropdown with your own UI.

Patterns

Debounced Auto-Save

ts
let saveTimeout: ReturnType<typeof setTimeout>;

const editor = init({
  container: '#editor',
  onChange(content) {
    clearTimeout(saveTimeout);
    saveTimeout = setTimeout(() => {
      saveToBackend(content);
    }, 2000);
  },
  onSave(content) {
    clearTimeout(saveTimeout);
    saveToBackend(content);
  },
});

Dirty State Tracking

ts
let isDirty = false;

const editor = init({
  container: '#editor',
  onChange() {
    isDirty = true;
    updateSaveButton();
  },
  onSave(content) {
    saveToBackend(content).then(() => {
      isDirty = false;
      updateSaveButton();
    });
  },
});

window.addEventListener('beforeunload', (e) => {
  if (isDirty) {
    e.preventDefault();
  }
});