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.