Form.io

Your Own App

Form.io enables you to build your own online and offline Web App with easy "drag and drop".

Your web app works across ALL devices e.g. iOS and Android phones, Windows and macOS laptops etc. ... even on most smart TVs!

Dynamic Drop-Down

We can use the Text Field component with "custom logic" to populate a regular Select Box dynamically.

1. Overview

1.1. URL Select Box

  1. Add a Text Field Component:

    • This will be a hidden field that stores the fetched data.
    • Go to Data → Default Value and set it to an empty array: [].
  2. Add a Select Box Component:

    • In the Select Box, set the Data Source Type to Values.
    • Add a single dummy value for now (e.g., { value: '', label: 'Loading...' }).
  3. Custom Logic to Populate Data:

  • Go to the Logic tab of the Text Field.
  • Add a new Custom Logic action:

Basic custom logic code:

const apiUrl = 'https://api.example.com/getOptions';

fetch(apiUrl)
  .then(response => response.json())
  .then(data => {
    if (Array.isArray(data.options)) {
      const options = data.options.map(item => ({
        label: item.label,
        value: item.id
      }));

      // Save the options in the Text Field
      data.value = options;

      // Update the Select Box
      const selectBox = form.getComponent('dynamicSelect');
      if (selectBox) {
        selectBox.component.data.values = options;
        selectBox.redraw();
      }
    }
  })
  .catch(err => console.error('Error fetching options:', err));

1.2. Notes

  1. Explanation

    • The fetch() call retrieves the data from the remote server.
    • The data.value stores the fetched options.
    • The Select Box is updated dynamically using .redraw().
  2. Tips

    • Hide the Text Field. Set the Hidden property to true for the Text Field.
    • Trigger the Fetch Logic. If you need the data to be fetched based on another component (e.g., a button click or another field), adjust the logic to trigger based on specific form events.
  3. Considerations

    • Ensure the API endpoint allows CORS requests.
    • Verify that the fetched data is correctly structured (i.e., an array of objects with label and value).
    • If the API requires authentication, adjust the fetch request to include necessary headers.

1.3. Example Response Structure

Assuming the response from the URL is:

{
  "data": [
    { "id": "1", "label": "Option 1" },
    { "id": "2", "label": "Option 2" },
    { "id": "3", "label": "Option 3" }
  ]
}
  • The Text Field will store the data as:
[
  { "value": "1", "label": "Option 1" },
  { "value": "2", "label": "Option 2" },
  { "value": "3", "label": "Option 3" }
]

2. Step by Step

Step 1: Add a Text Field Component

  • Label: Data Fetcher
  • Key: dataFetcher
  • Hidden: true (Check the Hidden property)

Step 2: Add a Select Box Component

  • Label: Dynamic Select

  • Key: dynamicSelect

  • Data Source Type: Values

  • Add a single placeholder value:

    • { value: '', label: 'Loading...' }

Step 3: Custom Logic to Populate Data

  • Go to the Logic tab of the Text Field (dataFetcher).
  • Click Add Action and select Custom Logic.

Custom Logic Code

Custom logic code with timeout and error handling:

const apiUrl = 'https://api.example.com/getOptions';
const timeoutDuration = 5000; // 5 seconds
const selectBox = form.getComponent('dynamicSelect');

// Show a loading indicator
selectBox.component.data.values = [{ value: '', label: 'Loading...' }];
selectBox.redraw();

// Fetch data with timeout
const fetchData = new Promise((resolve, reject) => {
  const timeout = setTimeout(() => reject(new Error('Request timed out')), timeoutDuration);

  fetch(apiUrl)
    .then(response => response.json())
    .then(data => {
      clearTimeout(timeout);
      resolve(data);
    })
    .catch(reject);
});

fetchData
  .then(data => {
    if (Array.isArray(data.options)) {
      const options = data.options.map(item => ({
        label: item.label,
        value: item.id
      }));

      selectBox.component.data.values = options;
    } else {
      throw new Error('Invalid data format');
    }
  })
  .catch(err => {
    console.error(err.message);
    selectBox.component.data.values = [{ value: '', label: 'Error loading data' }];
  })
  .finally(() => {
    selectBox.redraw();
  });

Explanation:

  • API URL: Replace 'https://api.example.com/getOptions' with the actual API endpoint.
  • Data Structure: The expected data structure is:
{
  "options": [
    { "id": "1", "label": "Option 1" },
    { "id": "2", "label": "Option 2" }
  ]
}
  • The fetched data is stored in the Text Field (dataFetcher) and simultaneously used to update the Select Box.

Step 4: Hide the Text Field

  • Set the Hidden property to true for the dataFetcher component.
  • This will prevent it from displaying but it will still execute the logic.

Step 5: Testing and Debugging:

  • Open the form in the preview mode.
  • Check the browser console for any errors during the fetch operation.
  • Verify that the Select Box updates dynamically with the fetched data.

Progressive Web Application

Progressive Web Application (PWA) provide extract capabilities (e.g. offline operation) not available in traditional web apps.

Although form.io supports a lot of external javascript frameworks:

It is possible to build a PWA with offline ability:

Pure form.io example

The following is a simple example of a PWA using form.io only.

formio/
├── app/                 ← Form.io backend logic
├── public/              ← PWA goes here
│   ├── index.html
│   ├── service-worker.js
│   ├── manifest.json
│   └── icon.png
├── server.js            ← Main Entry Point
├── .env                 ← Set MONGO_URI, JWT_SECRET, etc.
└── package.json

To run

npm install
node server.js

PWA now available at http://localhost:3001/index.html and the forms are available http://localhost:3001/form/:formName

server.js

const express = require('express');
const path = require('path');

const app = express();

//  1. Serve static frontend files (PWA)
app.use(express.static(path.join(__dirname, 'public')));

//  2. Load Form.io backend
require('./app')(app); // Loads Form.io routes, middleware, etc.

//  3. Fallback to index.html for unknown frontend routes (SPA support)
app.get('*', (req, res, next) => {
  if (req.path.startsWith('/form') || req.path.startsWith('/user') || req.path.startsWith('/submission')) {
    // Let Form.io handle its own API routes
    return next();
  }
  res.sendFile(path.join(__dirname, 'public/index.html'));
});

//  4. Start the server
const port = process.env.PORT || 3001;
app.listen(port, () => {
  console.log(` Form.io server with PWA running at http://localhost:${port}`);
});
  • public/ folder should contain index.html, service-worker.js, manifest.json, etc.
  • form.io backend is mounted by require('./app')(app) — this is the core Form.io API server.
  • fallback handler makes app behave like a single-page application (SPA) and serves index.html for all non-API routes.
  • add other routes to the if condition if using custom Express routes (e.g., /auth, /api, etc.)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form.io PWA</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="theme-color" content="#0074D9">
  <link rel="manifest" href="manifest.json">
  <link rel="icon" href="icon.png" type="image/png">
  <script src="https://cdn.form.io/formiojs/formio.full.min.js"></script>
  <style>
    body {
      font-family: sans-serif;
      margin: 1rem;
    }
  </style>
</head>
<body>
  <h1>Form.io Offline PWA</h1>
  <div id="formio">Loading form...</div>

  <script>
    // Render form from your self-hosted Form.io backend
    Formio.createForm(document.getElementById('formio'), 'http://localhost:3001/form/myform', {
      offline: true,
      saveDraft: true
    }).then(form => {
      form.on('submit', (submission) => {
        alert('Form submitted!');
        console.log(submission);
      });
    });

    // Register service worker
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('service-worker.js')
        .then(() => console.log(' Service Worker registered!'))
        .catch(err => console.error(' Service Worker registration failed:', err));
    }
  </script>
</body>
</html>

manifest.json

{
  "name": "Form.io PWA",
  "short_name": "FormApp",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0074D9",
  "icons": [
    {
      "src": "icon.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}

service-worker.js

const cacheName = 'formio-pwa-cache-v1';
const filesToCache = [
  '/',
  '/index.html',
  '/manifest.json',
  '/icon.png',
  'https://cdn.form.io/formiojs/formio.full.min.js'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(cacheName).then((cache) => cache.addAll(filesToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(response => response || fetch(event.request))
  );
});