Skip to main content

Autosave WYSIWYG Editors

WYSIWYG editors like TipTap and Quill require special handling for autosave. This guide shows you how to integrate them with Form Guardian.

TipTap Editor

TipTap is a modern WYSIWYG editor. Here's how to integrate it:

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useFormAutosave } from '@form-guardian/react';
import { useEffect, useCallback } from 'react';

function TipTapForm() {
const { formRef, restoreValues, clearDraft, saveValues } = useFormAutosave(
'tiptap-form',
{ autoRestore: false }
);

const editor = useEditor({
extensions: [StarterKit],
content: '',
onUpdate: ({ editor }) => {
// Save on every update (debounced by Form Guardian)
const content = editor.getHTML();
// Form Guardian will automatically save this
},
});

// Restore draft on mount
useEffect(() => {
if (editor) {
restoreValues(
(field, value) => {
if (field === 'content' && typeof value === 'string') {
editor.commands.setContent(value);
}
},
() => ({
content: editor.getHTML(),
})
);
}
}, [editor]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const content = editor?.getHTML() || '';
// Submit logic
await clearDraft();
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
<EditorContent editor={editor} />
<button type="submit">Submit</button>
</form>
);
}

Quill Editor

Quill is another popular WYSIWYG editor:

import { useQuill } from 'react-quill-new';
import 'react-quill-new/dist/quill.snow.css';
import { useFormAutosave } from '@form-guardian/react';
import { useEffect, useRef } from 'react';

function QuillForm() {
const quillRef = useRef<any>(null);
const { formRef, restoreValues, clearDraft } = useFormAutosave(
'quill-form',
{ autoRestore: false }
);

const { quill, quillRef: quillElementRef } = useQuill({
theme: 'snow',
});

useEffect(() => {
if (quill) {
quillRef.current = quill;

// Restore draft
restoreValues(
(field, value) => {
if (field === 'content' && typeof value === 'string') {
quill.root.innerHTML = value;
}
},
() => ({
content: quill.root.innerHTML,
})
);

// Save on text change
quill.on('text-change', () => {
// Form Guardian will automatically save
});
}
}, [quill]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const content = quill?.root.innerHTML || '';
// Submit logic
await clearDraft();
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
<div ref={quillElementRef} />
<button type="submit">Submit</button>
</form>
);
}

ContentEditable Element

For simple contentEditable elements:

import { useFormAutosave } from '@form-guardian/react';
import { useRef, useEffect } from 'react';

function ContentEditableForm() {
const editorRef = useRef<HTMLDivElement>(null);
const { formRef, restoreValues, clearDraft } = useFormAutosave(
'contenteditable-form',
{ autoRestore: false }
);

useEffect(() => {
if (editorRef.current) {
restoreValues(
(field, value) => {
if (field === 'content' && editorRef.current) {
editorRef.current.innerHTML = value as string;
}
},
() => ({
content: editorRef.current?.innerHTML || '',
})
);
}
}, []);

return (
<form ref={formRef}>
<div
ref={editorRef}
contentEditable
data-track="true"
style={{ border: '1px solid #ccc', minHeight: '200px', padding: '10px' }}
/>
<button type="submit">Submit</button>
</form>
);
}

With React Hook Form

Integrating with React Hook Form:

import { useForm } from 'react-hook-form';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useFormAutosave } from '@form-guardian/react';
import { useEffect } from 'react';

interface FormData {
title: string;
content: string;
}

function TipTapWithRHF() {
const { register, handleSubmit, setValue, getValues, watch } = useForm<FormData>({
defaultValues: {
title: '',
content: '',
},
});

const editor = useEditor({
extensions: [StarterKit],
content: '',
onUpdate: ({ editor }) => {
setValue('content', editor.getHTML(), { shouldDirty: true });
},
});

const { formRef, restoreValues, clearDraft } = useFormAutosave(
'tiptap-rhf-form',
{ autoRestore: false }
);

useEffect(() => {
if (editor) {
restoreValues(
(field, value) => {
if (field === 'content' && typeof value === 'string') {
editor.commands.setContent(value);
setValue('content', value, { shouldValidate: false });
} else {
setValue(field as keyof FormData, value, { shouldValidate: false });
}
},
() => getValues()
);
}
}, [editor]);

const onSubmit = async (data: FormData) => {
await submitForm(data);
await clearDraft();
};

return (
<form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
<input {...register('title', { required: true })} placeholder="Title" />
<EditorContent editor={editor} />
<button type="submit">Submit</button>
</form>
);
}

Handling Large Content

For large content, consider optimizing:

function OptimizedTipTapForm() {
const { formRef, restoreValues, clearDraft } = useFormAutosave(
'tiptap-form',
{
autoRestore: false,
debounceMs: 1000, // Longer debounce for large content
}
);

const editor = useEditor({
extensions: [StarterKit],
content: '',
onUpdate: ({ editor }) => {
// Only save HTML, not full editor state
const html = editor.getHTML();
// Form Guardian handles the rest
},
});

// Restore only HTML content, not full editor state
useEffect(() => {
if (editor) {
restoreValues(
(field, value) => {
if (field === 'content' && typeof value === 'string') {
editor.commands.setContent(value, false); // false = don't add to history
}
},
() => ({
content: editor.getHTML(),
})
);
}
}, [editor]);

return (
<form ref={formRef}>
<EditorContent editor={editor} />
</form>
);
}

Best Practices

  1. Save HTML, not editor state - Editor state can be large and editor-specific
  2. Use longer debounce - WYSIWYG content changes frequently
  3. Handle restoration carefully - Set content without adding to undo history
  4. Clear drafts after submission - Prevent stale content

Common Pitfalls

❌ Don't Save Full Editor State

// ❌ Wrong - editor state is large and editor-specific
const state = editor.getJSON();
saveDraft({ state });

✅ Save HTML Content

// ✅ Correct - HTML is portable and smaller
const html = editor.getHTML();
saveDraft({ content: html });

Next Steps