Offline Plugin

We provide a plugin to users with Enterprise projects that enables their applications to request and submit forms offline.

The plugin provides offline mode support for a single project. Offline requests will automatically kick in when a request fails due to a network error and serve data cached offline, if available. You may also create/edit/delete submissions while offline, and the requests will be queued to be sent when the app goes back online.

Indexeddb

The offline plugin uses indexeddb. This storage mechanism was chosen because it is supported by all browsers, is similar in functionality to mongodb and has no hard coded usage limits in any browsers. Unlike most other browser storage solutions, when the amount of data in indexeddb gets close to full it will prompt the user to allow more data usage. This allows very large amounts of data to be stored in indexeddb.

Browser Compatibility

Indexeddb is compatible with all modern browser.

  • Chrome v38+
  • Safari 7.1+
  • IE 10+
  • Firefox 13+

The offline plugin operates in two parts. First, queueing any submissions that fail due to a network connect, and second, caching all submissions from the server so they are available when offline.

Submission Queue

Whenever a submission is made, the submission is added to the end of the offline queue and the queue is attempted to be flushed to the server. If a network error occurs it will simply stop and leave the submission in the queue for the next try. The offline queue is a first in first out queue so the first item added will be the first sent to the server.

In the event that a submission error occurs when attempting to send the submission to the server, such as a validation failure, the “offline.formError” event will be triggered. This will then stop the queue flush and wait for the user to fix the error before proceeding.

Submission Caching

When submissions are fetched from the server the request is first sent to the server. If a network error occurs the request is redirected to the offline submission cache and the cached results are returned instead. If the request succeeded from the server the returned submissions are added the the offline cache and the results are returned to the application.

Offline Plugin API

The offline mode plugin uses the form.io renderer plugin system to hook into each request using the following registration process.

var plugin = new FormioOfflineProject('https://myproject.form.io', 'path/to/project.json');
Formio.registerPlugin(plugin, 'myproject-offline');

// You can later access the plugin with:
// var plugin = Formio.getPlugin('myproject-offline');

new FormioOfflinePlugin(projectUrl, [projectJsonPath])

The constructor accepts two arguments. The projectUrl should point to the project that offline mode should be enabled for. The optional projectJsonPath must be the path to a exported project.json file to use to initialize the offline forms (If your app starts while offline, the forms in the project.json can still be successfully requested).

Saving forms and submissions offline

Once you register a plugin for a particular project, all load requests for forms and submissions in that project will automatically save and update in an offline browser data store.

For example, you can have all submissions for a form available offline if you call formio.loadSubmissions() at some point in your app while online.

Loading forms and submissions offline

When your app goes offline, requests to load a form or submission will automatically return the data cached offline, if available.

Submitting forms offline

Form submissions work a little bit differently when this plugin is registered. All form submissions, submission edits, and submission deletions are added to a queue. If the app is online, the queue will be emptied immediately and behave like normal. But when the app is offline, the submissions stay in the queue until the app goes back online. Until then, Formio.js will behave as if the submission was successful.

The queue will automatically start to empty when a submission is successfully made online, or you may manually start it.

Handling submission queue errors

Some queued submissions may fail when they are sent online (ex: unique validation fails on the server). In the event a queued submission fails, the queue is stopped and events are triggered to allow your app to resolve the bad submission before continuing. It’s up to your app to decide how to handle these errors. Your app may decide to prompt the user to fix the form submission or simply ignore the submission, and restart the queue.

Plugin methods

plugin.forceOffline(offline)

Forces all requests for this plugin’s project into offline mode, even when a connection is available.

plugin.isForcedOffline()

Returns true if this plugin is currently forced offline.

plugin.clearOfflineData()

Clears all offline data. This includes offline cached forms and submissions, as well as submissions waiting in the offline queue.

plugin.dequeueSubmissions()

Starts to process the submission queue. All requests in the offline submission queue will be sent in the order they were made. Successful requests will either resolve their original promise or trigger the offline.formSubmission event from Formio.events. A failed request will stop processing the queue and trigger the offline.formError event. The app must handle this event to resolve the failing requests and restart the queue.

plugin.submissionQueueLength()

Returns the number of submission requests currently in the offline queue.

plugin.getNextQueuedSubmission()

Returns the next request in the submission queue.

plugin.setNextQueuedSubmission(request)

Sets the next request in the submission queue to request.

You can use this to fix a failed submission and then call dequeueSubmissions() to resubmit and continue the queue.

plugin.skipNextQueuedSubmission()

Discards the next request in the submission queue.

You can use this to ignore a failed submission and then call dequeueSubmissions() to continue to the next queued request.

Events

You can listen for these events by adding listeners to the Formio.events EventEmitter.

NOTE: if you are using the Angular ngFormio library, you can listen for these events in the Angular scope by adding formio. before each event name.

offline.queue

Triggered when a submission is added to the submission queue.

offline.dequeue

Triggered when the submission queue starts to process a submission.

offline.requeue

Triggered when a submission fails and is added back to the front of the submission queue.

offline.formSubmission

Triggered when a queued submission is successfully submitted. This is not called if the original promise of the request can be resolved (in which case it behaves like a normal online submission).

offline.formError

Triggered when a queued submission returns an error. This means the app needs to either fix the submission or skip it, and restart the queue.

offline.queueEmpty

Triggered when the queue becomes empty after dequeuing.

Request Options

skipQueue

You may set skipQueue to save a submission immediately, skipping the queue. This will disable offline queuing for that submission. For example:

formio.saveSubmission(submission, {skipQueue: true});

If you are using the Angular ngFormio library, you can set the skipQueue option with formio-options:

<formio src="userLoginForm" formio-options="{skipQueue: true}"></formio>

AngularJS Offline

Example Application

To help with the implementation of Offline Mode, we have contributed an Open Source application that incorportes our offline mode capabilities. This application is called the GPS Tracker. To quickly bootstrap this project to test locally you can run the following {{ site.formio }} CLI command.

npm install -g formio-cli
formio bootstrap formio/formio-app-gpstracker

The following documentation serves to show you how to add this capability into your own project.

Adding Offline Mode to your Project

To add offline mode to your project, you must first upgrade your project to Enterprise. Once you do this, you will then be added to our Offline Mode Github project which will allow you to include it within your application. To do that you will use bower and type the following.

bower install --save git@github.com:formio/formio-plugin-offline.git

You will also need to include the following project dependencies which will be needed to build the offline manifest as well as include the offline mode in your project.

npm install --save-dev gulp-manifest
bower install --save ng-formio-helper

Once you do this, you can then include the Offline mode into your application by including the ngFormioHelper library.

/src/app/index.module.js

(function() {
  'use strict';
  angular
    .module('myApp', [
      'ngFormioHelper'
    ]);
  })();

Create the application manifest

Now that the Offline mode has been added to your project, you will now need to create the applicatoin manifest for the application cache capability. We can use Gulp to help out with this, by including the following Gulp tasks within our build routines.

/gulp/build.js

gulp.task('offline', ['html', 'config', 'fonts', 'other'], function() {
  return gulp.src([path.join(conf.paths.dist, '/**/*')], { base: './dist/' })
    .pipe($.manifest({
      hash: true,
      preferOnline: true,
      network: ['*'],
      filename: 'app.manifest',
      exclude: ['app.manifest', 'maps/**']
    }))
    .pipe(gulp.dest(conf.paths.dist));
});

and then register it to the build task.

gulp.task('build', ['html', 'fonts', 'other', 'views', 'config', 'offline']);

We now need to add the manifest to our index.html file.

/src/index.html

<html manifest="app.manifest" ng-app="myApp">

Register Offline Mode within your application.

To get offline mode working in your application, we will use the Formio helper library to register our application with offline mode support. We start this within the config method of our Angular.js app which defines our routes.

/src/app/index.route.js

/** @ngInject */
function routeConfig(
  FormioOfflineProvider,
) {

  FormioOfflineProvider.register({
    errorUrl: '/error',
    homeState: 'home'
  });

We now need to initialize this within the application using the following.

/src/app/index.run.js

angular
  .module('myApp')
  .run([
    'FormioOffline',
    function(
      FormioOffline
    ) {
      // Initialize offline mode for your application.
      FormioOffline.init();

Add the offline button to the header

The next thing you need to do is add the Offline mode button to your header of your application. This will allow you to syncronize the submissions that have been captured within offline mode. You can do that with the following <offline-button> directive code in your applications navbar.

<div id="navbar" class="navbar-collapse collapse">
  <ul class="nav navbar-nav navbar-right">
    <li><offline-button></offline-button></li>
  </ul>
</div>

Once you do that you should now have offline capabilities within your application!

Javascript Only Offline

The offline plugin integrates directly with the Formio library so it is available without any framework as well. The following is an example html page that uses unpkg files to load all dependencies except the offline plugin. It uses a simple router (navigo) to create a few pages. It is an example of how to implement some of the offline functions and events to enable offline mode.

In order to run this, you will need to install the offline plugin into the node_modules folder.

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="//unpkg.com/formiojs@latest/dist/formio.full.min.css">
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <title>Form.io Offline Example</title>
</head>
<body>
<nav class="navbar navbar-default">
  <div class="container">
    <ul class="nav navbar-nav">
      <li><a href="" data-navigo>Form Entry</a></li>
      <li><a href="queue" data-navigo>Offline Queue (<span id="count">0</span>)</a></li>
      <li><a href="error" data-navigo>Offline Error</a></li>
      <li><a href="thanks" data-navigo>Thanks</a></li>
    </ul>
  </div>
</nav>
<div class="page-content">
  <div id="content" class="container">
  </div>
</div>
<!--<script type="text/javascript" src="cordova.js"></script>-->
<script type="text/javascript" src="//unpkg.com/formiojs@latest/dist/formio.full.min.js"></script>
<script type="text/javascript" src="//unpkg.com/navigo@latest/lib/navigo.min.js"></script>
<script type="text/javascript" src="node_modules/formiojs-plugin-offline/dist/formiojs-plugin-offline.js"></script>
<script type="text/javascript">
  var projectUrl = 'https://offline-demo.form.io';

  // Initialize a simple router.
  var router = new Navigo();

  // Configure the offline plugin.
  var offline = new FormioOfflineProject(projectUrl, 'project.json');
  Formio.registerPlugin(offline, 'offline-demo');

  var setOfflineCount = function() {
    offline.ready.then(function() {
      document.getElementById('count').innerHTML = offline.submissionQueueLength();
    });
  }

  // Update the count when items go through queue.
  Formio.events.on('offline.queue', setOfflineCount);
  Formio.events.on('offline.dequeue', setOfflineCount);
  Formio.events.on('offline.formSubmission', setOfflineCount);

  Formio.events.on('offline.queueEmpty', function() {
    // When queue is empty, go home.
    setOfflineCount();
    router.navigate();
  });

  // Normally error information would be passed as part of the route parameters but the simple router doesn't allow it.
  var errorItem = {};
  Formio.events.on('offline.formError', function(error, request) {
    // If an offline error occurs, go to the error page and present the submission that caused the error so it can be fixed.
    errorItem = {error: error, request: request.request};
    router.navigate('error');
  });

  router
    .on({
      'queue': offline.dequeueSubmissions,
      'error': function() {
        document.getElementById('content').innerHTML = '' +
          '<h2>An error occurred submitting the offline Queue</h2>' +
          '<p>Please fix it and continue</p>' +
          '<div class="alert alert-danger" id="alert"></div>' +
          '<div id="formio" />';
        document.getElementById('alert').innerHTML = "";

        errorItem.error.details.forEach(function(detail) {
          var element = document.createElement('div');
          element.innerHTML = detail.message;
          document.getElementById('alert').appendChild(element);
        });

        Formio.createForm(document.getElementById('formio'), errorItem.request.formUrl)
          .then(function(form) {
            // We will update the item in the queue instead of attempting a new submission.
            form.nosubmit = true;
            form.submission = errorItem.request.data;

            form.on('submit', function(submission) {
              errorItem = {};
              offline.dequeueSubmissions();
            })
          });
      },
      'thanks': function() {
        document.getElementById('content').innerHTML = 'Thanks';
      },
      '*': function() {
        document.getElementById('content').innerHTML = '<div id="formio"></div>';
        Formio.createForm(document.getElementById('formio'), projectUrl + '/survey')
          .then(function(form) {
            form.on('submitDone', function() {
              setOfflineCount();
              router.navigate('/thanks');
            })
          });
      }
    });

  // Init navigate home.
  router.navigate();
  // Init the count.
  setOfflineCount();

</script>
</body>
</html>

Cordova App Offline

The offline plugin also supports cordova applications developed for mobile platforms. In order for it to work you will need the cordova-plugin-indexeddb-async plugin.

We have a demo application for how to set up a cordova offline application. You can add a frontend framework to this as well but this app is javascript only within cordova.

Cordova Offline App