Skip to content

Local first form builder #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions blogpost-apps/local-first-form-builder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SurveyJS Form Builder – Offline Preact Demo

This is a lightweight demo showcasing how to use the [SurveyJS Form Builder](https://surveyjs.io/open-source) in an **offline-first Preact** setup.

## Features

- How to self-host the SurveyJS Form Builder
- How to work completely offline (no internet required)
- How to save/load survey JSON and theme to/from `localStorage`
- How to sync survey data manually when needed

## Related Blog Post
Read the full tutorial here: [Build Truly Offline Web Forms with SurveyJS](https://surveyjs.io/stay-updated/blog/local-fist-form-builder#try-the-demo-offline-surveyjs-creator).

## Tech Stack

- Preact
- SurveyJS Form Library
- LocalStorage API

## How to Run

1. Clone this repository
2. Open `index.html` directly in a browser — no server required

This demo is static and runs 100% in the browser.
131 changes: 93 additions & 38 deletions blogpost-apps/local-first-form-builder/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SurveyJS Creator V2 (pReact version)</title>
<title>SurveyJS Form Builder (pReact version)</title>

<script src="./survey.core.js"></script>
<script src="./survey.i18n.js"></script>
Expand All @@ -21,52 +21,107 @@
<body>
<div id="surveyCreatorContainer" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;"></div>

<script>
const LOCAL_STORAGE_KEY = "localSurveyJSON";
const SYNCED_FLAG_KEY = "localSurveySynced";
<script>
const LOCAL_STORAGE_KEY = "localSurveyJSON";
const THEME_STORAGE_KEY = "localSurveyTheme";
const SYNCED_FLAG_KEY = "localSurveySynced";

SurveyCreatorCore.registerCreatorTheme(SurveyCreatorTheme);
let isSurveySaved = false;
let isThemeSaved = false;

const creator = new SurveyCreator.SurveyCreator();
SurveyCreatorCore.registerCreatorTheme(SurveyCreatorTheme);
SurveyCreatorCore.registerSurveyTheme(SurveyTheme);

creator.autoSaveEnabled = true;
const creator = new SurveyCreator.SurveyCreator({
showThemeTab: true,
showTranslationTab: true
});

const savedJSON = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedJSON) {
try {
creator.JSON = JSON.parse(savedJSON);
} catch (e) {
console.warn("Failed to parse saved JSON from localStorage:", e);
}
creator.autoSaveEnabled = true;

// Restore saved JSON
const savedJSON = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedJSON) {
try {
creator.JSON = JSON.parse(savedJSON);
} catch (e) {
console.warn("Failed to parse saved JSON:", e);
}
}

// Restore theme
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme) {
try {
creator.theme = JSON.parse(savedTheme);
} catch (e) {
console.warn("Failed to parse saved theme:", e);
}
}


function markSyncedIfBothSaved() {
if (isSurveySaved && isThemeSaved) {
localStorage.setItem(SYNCED_FLAG_KEY, "true");
console.log("Both survey and theme saved. Synced flag set.");
} else {
localStorage.setItem(SYNCED_FLAG_KEY, "false");
}
}

creator.saveSurveyFunc = (saveNo, callback) => {
// Persist a survey JSON
creator.saveSurveyFunc = (saveNo, callback) => {
try {
const currentJSON = creator.JSON;
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(currentJSON));
localStorage.setItem(SYNCED_FLAG_KEY, "false");
console.log("Survey auto-saved locally.");
callback(saveNo, true);
};

window.addEventListener("online", () => {
const json = localStorage.getItem(LOCAL_STORAGE_KEY);
const isSynced = localStorage.getItem(SYNCED_FLAG_KEY);

if (json && isSynced !== "true") {
sendToServer(JSON.parse(json));
}
});

function sendToServer(data) {
console.log("Syncing with server...", data);

setTimeout(() => {
console.log("Sync complete.");
localStorage.setItem(SYNCED_FLAG_KEY, "true");
}, 1000);
isSurveySaved = true;
console.log("Survey saved.");
} catch (e) {
console.error("Failed to save survey JSON:", e);
isSurveySaved = false;
}
markSyncedIfBothSaved();
callback(saveNo, true);
};

// Persist a survey theme
creator.saveThemeFunc = (saveNo, callback) => {
try {
const currentTheme = creator.theme;
localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(currentTheme));
isThemeSaved = true;
console.log("Theme saved.");
} catch (e) {
console.error("Failed to save theme:", e);
isThemeSaved = false;
}
markSyncedIfBothSaved();
callback(saveNo, true);
};

window.addEventListener("online", () => {
const isSynced = localStorage.getItem(SYNCED_FLAG_KEY);
const json = localStorage.getItem(LOCAL_STORAGE_KEY);
const theme = localStorage.getItem(THEME_STORAGE_KEY);

if (isSynced !== "true" && json && theme) {
console.log("Syncing with server...");
sendToServer(JSON.parse(json), theme);
}
});

function sendToServer(surveyData, theme) {
// Simulated async sync
setTimeout(() => {
console.log("Synced survey:", surveyData);
console.log("Synced theme:", theme);
localStorage.setItem(SYNCED_FLAG_KEY, "true");
}, 1000);
}

creator.render("surveyCreatorContainer");
</script>


creator.render("surveyCreatorContainer");
</script>
</body>
</html>
Loading