SurveyJS Server Integration · PHP / Laravel I.3 · Resume where the user left off
You work in a private copy of the demo database, kept until it has been unused for 24 hours.

I.3

Resume where the user left off

You need this: Long or multi-page forms that people fill in over several sittings.

Save the partial answers together with the survey's UI state (current page, active input) while the user works, and restore both when they return. Save on value changes and on page changes, debounced, and flush when the tab is hidden so closing it loses nothing. The server stores one JSON document per user and form; the samples key it by form for brevity.

This step on the Server Integration page Source on GitHub

Try this

  1. Type in a few fields. Requests shows one PUT /api/progress/onboarding per pause, not one per keystroke, and the indicator below says when it saved.
  2. Go to the next page: the page change saves too.
  3. Type a letter, then switch to another browser tab and come back at once: the PUT was flushed when the tab was hidden.
  4. : you are back on the same page, in the same input, with your answers.
  5. to start over.

Progress:

Code that just ran

These regions are read from the files that served this page, the same lines the Server Integration page shows.

Client · shared/client/resume-progress.js · sjs:I.3.client View on GitHub
import debounce from "lodash/debounce";

// On return: restore both — the user continues on the same page, in the same input.
// Restore before listening, so restoring the page doesn't count as a change to save.
const saved = await fetch(`/api/progress/${formId}`).then(r => (r.ok ? r.json() : null));
if (saved) {
  survey.data = saved.data;
  survey.uiState = saved.uiState;
}

// Save answers + UI state as the user works (uiState: see the Form Library API reference)
const saveProgress = debounce(() => {
  fetch(`/api/progress/${formId}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ data: survey.data, uiState: survey.uiState }),
    keepalive: true                    // lets the last request finish while the tab closes
  });
}, 800);
survey.onValueChanged.add(saveProgress);
survey.onCurrentPageChanged.add(saveProgress);
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") saveProgress.flush();
});
Server · routes/examples/resume-progress.php · sjs:I.3.server View on GitHub
// GET /api/progress/{formId} — return the stored progress (answers + UI state) (404 if none yet)
Route::get('/api/progress/{formId}', function (string $formId) {
    $json = DB::table('progress')->where('key', $formId)->value('json') ?? abort(404, 'No progress saved yet');

    return response($json)->header('Content-Type', 'application/json');
});

// PUT /api/progress/{formId} — store the progress (answers + UI state) as-is: it's one JSON document
// Real code keys it by user and form, e.g. Auth::id().':'.$formId; the samples key by form for brevity
Route::put('/api/progress/{formId}', function (Request $request, string $formId) {
    // The raw body, not $request->input(): PHP arrays would turn {} into [] in the stored JSON
    $progress = json_decode($request->getContent(), flags: JSON_THROW_ON_ERROR);
    DB::table('progress')->updateOrInsert(['key' => $formId], [
        'json' => json_encode($progress, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR),
    ]);

    return response()->noContent();
});

Definition: shared/definitions/resume-progress.json