Skip to content
Working With Vue

Multi-step forms in Vue

Build Vue multi-step forms with Qubeforms encrypted drafts. Save reactive field state, handle draft tokens and expiration, and complete submissions without stale updates.

1. Keep step state in one place

Use the field state and submit handler from A Vue form component as the starting point. Keep the values for earlier steps in the parent even if their inputs are no longer rendered. Use explicit Save and continue controls with type="button" so they do not trigger a final submission. Drafts are available on every plan, but final attachments still require a paid plan. See Multi-step drafts for the endpoint reference and limits.

2. Create a draft saver

Save this helper as draftSaver.js and import createDraftSaver into your component. Create one instance per visitor/form component, not a module-level singleton shared by server-rendered requests. It creates a draft once, merges later updates, and attaches the draft credentials to the final multipart body. It serializes saves and surfaces errors rather than sending concurrent updates. Pass the bare public key, not the full endpoint.

export function createDraftSaver(publicKey) {
  const base = `https://api.qubeforms.io/v1/${encodeURIComponent(publicKey)}`;
  let draft = null;
  let saving = false;

  return {
    async save(values, step) {
      if (saving) throw new Error('A draft save is already in progress.');
      saving = true;
      try {
        const updating = draft !== null;
        const response = await fetch(updating ? `${base}/drafts/${draft.draft_id}` : `${base}/drafts`, {
          method: updating ? 'PATCH' : 'POST',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            ...(updating ? { 'X-Qubeforms-Draft-Token': draft.draft_token } : {}),
          },
          body: JSON.stringify({ ...values, _cf_step: step }),
        });
        const result = await response.json().catch(() => null);
        if (!response.ok || !result?.draft_id || (!updating && !result.draft_token)
            || (updating && result.saved !== true)) {
          throw new Error(result?.error?.message || `Draft save failed (HTTP ${response.status}).`);
        }
        if (!updating) draft = result;
        return { draft_id: draft.draft_id, expires_in: draft.expires_in };
      } finally {
        saving = false;
      }
    },
    attachTo(body) {
      if (saving) throw new Error('Wait for the draft save to finish.');
      if (!draft) throw new Error('Save the first step before finishing.');
      body.set('_cf_draft_id', draft.draft_id);
      body.set('_cf_draft_token', draft.draft_token);
    },
    clear() { draft = null; },
  };
}

3. Connect a step button

Add this adapter to the basic Vue component and import the helper from ./draftSaver. Call it from a Save and continue button, using the appropriate _cf_step number for each step. Validate the visible fields before calling it, since a plain button does not run native submit validation. Disable both save and submit controls while saving or submitting. Advance the step only after the promise succeeds; leave values untouched on failure.

// Inside <script setup>, create one saver for this component instance.
const saver = createDraftSaver('cf_pk_YOUR_KEY');
const savingDraft = ref(false);

async function saveNextStep() {
  if (savingDraft.value || sending.value) return;
  savingDraft.value = true;
  try {
    await saver.save({ ...fields }, 1);
    feedback.value = 'Draft saved. Continue to the next step.';
    // Advance your step state here, after the save succeeds.
  } catch (error) {
    feedback.value = error instanceof Error ? error.message : 'Could not save the draft.';
  } finally {
    savingDraft.value = false;
  }
}

4. Complete the saved draft

In the final submit handler, add the credentials to FormData before sending it to /submissions. Run the attachment call inside your error-handled request flow and clear the saver only after a confirmed successful final response. The saved draft supplies fields omitted by the final step; visible final values override them. Send fresh CAPTCHA data when required. When a visitor skips draft saving, use the ordinary submission flow instead of calling attachTo.

// Before fetch, inside the submit handler's try block:
saver.attachTo(body);

// Only after response.ok and result.ok === true:
saver.clear();

5. Handle expiration and resuming

Drafts normally expire 72 hours after creation; saving again does not extend that deadline. The create response supplies expires_in. The helper keeps draft credentials only in memory, so a reload or component unmount starts over. There is no customer GET endpoint for retrieving saved draft fields and no automatic resume email. To resume across reloads, your application must securely retain its own display state and credentials. Keep tokens out of URLs and logs, and do not describe an in-memory draft as reload-proof.

6. Avoid stale writes and repeated submissions

Save at step boundaries rather than on every keypress. Wait for a save to finish before updating or submitting again. HTTP 404 can mean a missing/wrong token or a purged draft, HTTP 410 an expired retained draft, and HTTP 429 a save or storage limit. After expiration, explicitly start a new draft while preserving the user’s local field state. Retrying a final request after an uncertain response can duplicate a submission; inspect receipt or implement the idempotency behavior described in Handling errors in Vue.

7. Test the full journey

Save the first step, change a saved value on the second step, finish the submission, and confirm merged values in the inbox. Test back navigation without losing local values, duplicate save clicks, an expired draft, and a reload. Files must remain selected in the browser until the final upload and may need reselection after navigation. In React, avoid creating or saving a draft from a rendering effect; in Vue, avoid a deep watcher that saves every field mutation.

Keep private credentials private

Public form keys can appear in browser code. Read keys, management credentials, and webhook signing secrets must stay on the server.