ProtoCrudModal

Modal editor for a single collection item with automatic draft persistence — unsaved changes survive tab switches and browser refreshes.

ProtoCrudModal

A modal editor for a single collection item. Features:

  • Auto-save draft on every change (to Y.Map 'draft:{draftKey}' in the document)
  • Resume/discard draft banner on next open
  • 2-column layout for wide modals (modalSize'lg')
  • Save/Cancel footer with keyboard shortcut support
  • Works with all field types including linked-responses

When to use it

Set editMode: 'modal' on a CollectionSchema to have ProtoCrudList automatically open ProtoCrudModal when a user clicks an item. You rarely need to instantiate ProtoCrudModal directly.

collections: {
  interviews: {
    label: 'Interviews',
    fields: {},
    editMode:  'modal',   // triggers ProtoCrudModal
    modalSize: '3xl',
  }
}

Props (when using directly)

PropTypeDefaultDescription
openbooleanrequiredControls modal visibility
schemaCollectionSchemarequiredThe collection schema
itemCollectionItem | nullnullExisting item to edit (null = new item)
docY.DocrequiredThe Y.js document (for draft persistence)
draftKeystring'default'Key suffix for the draft Y.Map entry

Emits

EventPayloadDescription
saveCollectionItemEmitted with merged data when user clicks Save
cancelEmitted when user cancels or closes

Draft persistence

When a user edits a form in the modal and then closes it without saving (navigates away, switches tabs, closes the browser), the in-progress edits are stored as a draft.

Storage location: Y.Map 'draft:{draftKey}' inside the shared Y.Doc.

Because drafts live in the Y.js document (backed by IndexedDB), they:

  • Survive page refreshes and browser restarts
  • Persist even when offline
  • Sync to the server when connectivity is restored (so drafts survive across devices if the user has an account)

Draft banner: On next open, if an unsaved draft exists for the item, a banner appears at the top of the modal:

┌─────────────────────────────────────────────────────┐
│ ℹ You have an unsaved draft from 3 hours ago.       │
│                           [Resume]  [Discard]       │
└─────────────────────────────────────────────────────┘

Clicking Resume fills the form with the draft data. Clicking Discard clears the draft and fills the form with the saved item data (or empty for a new item).

Layout behavior

modalSizeLayout
sm, md, lgSingle-column fields
xl, 2xl, 3xl, 4xl, 5xlTwo-column fields (fields with colSpan: 2 span both columns)

Tailwind max-width mapping

modalSizeMax width
smmax-w-sm
mdmax-w-md
lgmax-w-lg
xlmax-w-xl
2xlmax-w-2xl
3xlmax-w-3xl
4xlmax-w-4xl
5xlmax-w-5xl

useProtoDraft

The draft composable, used internally by ProtoCrudModal:

function useProtoDraft(
  doc:      Y.Doc,
  draftKey: string,
): ProtoDraftState

interface ProtoDraftState {
  draft:       Ref<Record<string, any> | null>
  hasDraft:    ComputedRef<boolean>
  draftAge:    ComputedRef<string | null>   // e.g. '3 hours ago'
  saveDraft(data: Record<string, any>): void
  clearDraft(): void
}

You can use useProtoDraft directly in your own custom modals:

const { doc } = useProtoDoc('my-tool')
const { draft, hasDraft, draftAge, saveDraft, clearDraft } =
  useProtoDraft(doc, `interview-${itemId}`)

// Auto-save as user types
watch(formData, (data) => saveDraft(data), { deep: true })

// On open — check for existing draft
onMounted(() => {
  if (hasDraft.value) {
    // Show resume/discard UI
  }
})

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.