Guide

Patterns

Real-world integration patterns for saving, uploading, and persisting editable content.

Image Upload

When a user crops or replaces an image, the save handler receives a Blob. Upload it to your server and get a URL back. The flow is always blob → upload → URL.

async function handleSave(data: SaveResult) {
  for (const [key, fields] of data) {
    for (const [name, content] of Object.entries(fields)) {
      if (content.type === 'image-blob') {
        // Upload the cropped image blob to your backend
        const formData = new FormData();
        formData.append('file', content.blob, 'image.webp');
        formData.append('alt', content.alt);

        const res = await fetch('/api/images/upload', {
          method: 'POST',
          body: formData
        });
        const { url } = await res.json();

        // Update local state with the new URL
        myData.image = { src: url, alt: content.alt };
      }
    }
  }
}

Note. The image editor exports WebP via OffscreenCanvas. The blob in image-blob is ready to upload directly — no additional processing needed.

Backend Integration

A full save handler that transforms SaveResult into an API call. The pattern below handles text, unchanged images, and newly cropped images in a single pass.

localStorage Demo

My Blog Post

Edit this content, then save to see it persist in localStorage.

<script lang="ts">
  import * as Editable from 'editable-kit/editable';
  import type { SaveResult } from 'editable-kit';

  let editing = $state(false);

  async function handleSave(allData: SaveResult) {
    const payload: Record<string, unknown> = {};

    for (const [key, fields] of allData) {
      const section: Record<string, unknown> = {};
      for (const [name, content] of Object.entries(fields)) {
        if (content.type === 'text') {
          section[name] = content.content; // ProseMirrorJSON
        } else if (content.type === 'image-blob') {
          // Upload blob, get URL back
          const url = await uploadImage(content.blob);
          section[name] = { src: url, alt: content.alt };
        } else if (content.type === 'image-src') {
          section[name] = { src: content.src, alt: content.alt };
        }
      }
      payload[key] = section;
    }

    // Save to your backend
    await fetch('/api/content', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });

    editing = false;
  }
</script>

<Editable.Root {editing} onsave={handleSave}>
  <!-- your editors here -->
</Editable.Root>

localStorage Persistence

A helper pattern for client-side persistence. Useful for prototyping and demos — the live example above uses this approach. For images, convert blobs to data URLs before storing.

// Working localStorage demo pattern
function loadData<T>(key: string, fallback: T): T {
  if (typeof window === 'undefined') return fallback;
  const stored = localStorage.getItem(key);
  return stored ? JSON.parse(stored) : fallback;
}

function saveData(key: string, data: unknown) {
  localStorage.setItem(key, JSON.stringify(data));
}

async function handleSave(allData: SaveResult) {
  for (const [key, fields] of allData) {
    const section: Record<string, unknown> = {};
    for (const [name, content] of Object.entries(fields)) {
      if (content.type === 'text') {
        section[name] = content.content;
      } else if (content.type === 'image-src') {
        section[name] = { src: content.src, alt: content.alt };
      } else if (content.type === 'image-blob') {
        // Convert blob to data URL for localStorage
        const reader = new FileReader();
        const dataUrl = await new Promise<string>((resolve) => {
          reader.onload = () => resolve(reader.result as string);
          reader.readAsDataURL(content.blob);
        });
        section[name] = { src: dataUrl, alt: content.alt };
      }
    }
    saveData(key, section);
  }
}

Error Handling

Keep the user in editing mode on failure so they can retry. Only set editing = false after the save succeeds.

async function handleSave(allData: SaveResult) {
  try {
    for (const [key, fields] of allData) {
      for (const [name, content] of Object.entries(fields)) {
        if (content.type === 'image-blob') {
          const url = await uploadImage(content.blob);
          // Update local state on success
          myData[name] = { src: url, alt: content.alt };
        }
      }
    }
    editing = false;
  } catch (error) {
    // Show error to user — don't exit editing mode
    console.error('Save failed:', error);
    showToast('Failed to save. Please try again.');
    // User can retry or cancel manually
  }
}

Important. Never exit editing mode before the save succeeds. If you set editing = false before the async operation completes, the user loses their work.