XForms - Offline Mode Documentation

XForms Author Guide

Enabling Offline Support

So a form can be use offline, you need to add the following attribute on the first model of your form: xxforms:offline="true".

For a form to be used offline, additional JavaScript libraries need to be included in the page (e.g. to be able to evaluate XPath expressions on the client) and the server needs to send additional information to the client about the form (e.g. MIPS). The attribute xxforms:offline="true" is added so those forms or applications that are not using the offline capabilities don’t have to pay an unnecessary penalty.

When Gears is accessed for the first time by any applications, users are asked if they want to allow the application to use Gears. The client-side code will attempt to use Gears only if xxforms:offline="true" is set. This way we avoid the case of users who have Gears installed being asked to allow the application to access Gears for forms that don’t even have the potential of going offline.

New XForms Events

We introduce 2 new events: xxforms-offline and xxforms-online. Those events are dispatched to all the models, respectively before the form is taken offline and after it is taken back online. As a form author you will listen on those events to be notified when the form is taken offline or online, for instance to update the state of the form in the database.

New XForms Actions

We introduce 3 actions:

Buttons To Take Form Online or Offline

If you want users to be able to take a form offline and online while viewing the form itself, you will add triggers for this on the form. (What follows is about buttons on the form itself. See the section Summary Page Author Guide for information on how to implement a similar functionality in an online or offline summary page.)

The Let’s see first what a “Take offline” button can look like:

<xforms:trigger ref=".[instance('is-online') = 'true']">
<xforms:label>Take offline</xforms:label>
<xxforms:offline ev:event="DOMActivate"/>
</xforms:trigger>

Note the XPath expression in the ref attribute. It is there to ensure that the button is visible only if the form is online. It makes a reference to the is-online instance which you could declare as:

<xforms:instance id="is-online">
<is-online>true</is-online>
</xforms:instance>

If you want to perform certain tasks when the form is taken offline, you will do this in the handler for the xxforms:offline event. If you are using the is-online instance, your event handler should at least update this instance:

<xforms:event ev:event="xxforms:offline">
<xforms:setvalue ref="instance('is-online')">false</xforms:setvalue>
[ Other actions that update the database when for is taken offline ]
</xforms:event>

When taking a form back online, you might have 2 different cases: either users want the changes they have done offline to be saved, or they want those changes to be discarded. You can implement this by proving 2 buttons. The button “Take online and save” would look like:

<xforms:trigger ref=".[not(xxforms:is-online())]">
<xforms:label>Take online and save</xforms:label>
<xforms:action ev:event="DOMActivate">
<xxforms:online/>
<xforms:dispatch name="online-save" target="main-model"/>
</xforms:action>
</xforms:trigger>

The event online-save is a custom event of your choosing. You would dispatch this event, and implement an event handler for online-save in which you will perform the actions you want to be done when the form is taken online and saved. Similarly you would have another trigger “Take online and discard” that takes the form online and dispatches the event online-discard.

If you are using the is-online instance, you also want to declare event handler for xxforms:online that updates this instance:

<xforms:event ev:event="xxforms:online">
<xforms:setvalue ref="instance('is-online')">true</xforms:setvalue>
</xforms:event>

The <xxforms:online/> and <xxforms:offline-save/> actions can only be used in the event handler for a DOMActivate event on an <xforms:trigger>. This action needs to be non-conditional (i.e. without an “if” attribute that would prevent it from running), it needs to be executed directly (not indirectly through some other event that would be dispatched when the trigger is activated), and the event handler must be a descendant or ancestor of the trigger (observers are not supported).

Button To Commit Changes to the Offline Store

When users are offline and are doing changes to the form, their changes are not stored to Gears until they press on a “Save” button. You need to add this button to the page. This button would run just one action: <xxforms:offline-save>. For instance:

<xforms:trigger ref="if (xxforms:instance('is-online') = 'false') then . else ()">
<xforms:label>Save</xforms:label>
<xxforms:offline-save ev:event="DOMActivate"/>
</xforms:trigger>

The expression on the ref attribute is there to make sure that this button only shows when the form is offline. This action doesn’t “do” anything from the perspective of the server-side component of the XForms engine; it is used as a way to mark the trigger that you want to use to perform that action, so the client-side code can save the changes done so far when the button is pressed.

Evaluating MIPS Offline

Orbeon Forms has the ability to evaluate standard XForms MIPS declared with <xforms:bind> when the form is offline. For MIPS to be evaluate offline, you need to add the following attribute to the corresponding <xforms:bind>: xxforms:offline="true". There are two constraints on XPath expressions used in MIPS marked for offline evaluation:

  • The expressions also need to be valid XPath 1.0 expressions.
  • The expressions can’t use any path expression other than ”.”, i.e. they can't use ”..”, ”/a/b”... (However you can refer to the the value of other controls by variable name; more on this in what follows.)

There are discussions in the XPath Working Group about adding a “name” attribute to <xforms:bind>. This will have the effect of declaring a variable of the same which can then be used in XPath expressions. Orbeon Forms already implements this today. For variables to be accessible to XPath expressions evaluated offline, they need to point to a node to which a control is bound. Imagine you have the following 3 controls:

<xforms:input ref="unit-count"/>
<xforms:input ref="unit-price"/>
<xforms:output ref="total-price"/>

If you would like the total price to be computed as the unit count multiplied by the unit price, you would write:

<xforms:bind nodeset="unit-count" name="unit-count"/>
<xforms:bind nodeset="unit-price" name="unit-price"/>
<xforms:bind nodeset="total-price" calcultate="$unit-count * $unit-price" xxforms:offline="true"/>

Summary Page Author Guide

Let’s make the distinction between an “online summary page” and an “offline summary page”. Both link to instances of forms (or just “forms” hereafter), and provide some information about the forms they are linking to. The online summary page can only be used with a network connection, while the offline summary page also works offline. Typically:

  • The online summary page lists all forms, and allows users to take forms offline or online.
  • The offline summary page only lists forms that have been taken offline, and allows user to take those back online.

Let’s see how you can:

  • Take a form offline (typically from the online summary page).
  • List forms taken offline (so you can generate the offline summary page).
  • Take a form online (typically from the offline summary page).

JavaScript Files to Include

You need to include the following JavaScript files in your summary page. This example uses relative URLs, which you will need to adjust depending on the URL of your summary page. (The directory ops/javascript is at the root of your web application.)

<script type="text/javascript" src="../ops/javascript/yui/yahoo.js"></script>
<script type="text/javascript" src="../ops/javascript/yui/event.js"></script>
<script type="text/javascript" src="../ops/javascript/yui/dom.js"></script>
<script type="text/javascript" src="../ops/javascript/encryption.js"></script>
<script type="text/javascript" src="../ops/javascript/xforms.js"></script>

Taking a Form Offline

You do this by calling:

ORBEON.xforms.Document.takeOfflineFromSummary(url);

Where url is the full URL of the form you want to take offline. In the offline summary page you will want to display some information about the form, in addition to a link to the form. It is your responsibility to save this additional information. You will do so by accessing directly the Gears API, creating a table to hold this information.

When you take a form offline, all the resources used by this form will be “captured”, i.e. stored locally so they can be accessed when offline. This will include all the resources linked in the page with the script, link, and img elements. It won’t include resources linked form CSS, such as background images.

Listing Forms Taken Offline

You do this by simply iterating over the data you have added to your table when taking a form offline. You can see how this in done in apps/fr/offline/summary.html. Note that this page is provided as an example. Our intent with Orbeon Forms is to merge the offline summary page and online summary pages into one page that would look like the online summary page but has reduced capabilities when used offline. Since this goes beyond the scope of this project, we will postpone work on a “unified” summary page until we are done with the required functionality. The following screenshot shows the example offline summary page implemented in summary.html. You can access this page from http://localhost:8080/orbeon/apps/fr/offline/summary.html.

Take a Form Online

Taking a form online is similar to taking it offline. It is done with:

ORBEON.xforms.Document.takeOnlineFromSummary(url)

The function takes two additional optional arguments:

  1. beforeOnlineListener - You can also pass a call-back function as the second argument to takeOnlineFromSummary(). That function will be called once the form is online. This will be your opportunity to dispatch an event such as “save data” or “discard data” to the form. This function gets as its first argument the window in which the form is loaded. If you wish to dispatch events to the form, you need to do it based on that window, otherwise the events will be dispatched to the current form inside the "summary page"
  2. formOnlineListener - A listener called after the form has been taken online.

Aborting Going Online

After a form is taken online the form is not necessarily online. It can be still offline if taking the form offline failed. Taking the form online will fail if:
  • The client can't communicate normally with the server, or the server is unable to process the request from the client.
  • Your XForms code decides that the form should not be allowed to be taken online. You do this by running the action <xxforms:offline/> during the Ajax request that "takes the form online".

For instance, assume you want to save data by running a submission when the form is taken online, but you want to leave the form offline if the submission fails. You would do this by taking the form online with:

ORBEON.xforms.Document.takeOnlineFromSummary(FORM_URL, function(formWindow) {
    formWindow.ORBEON.xforms.Document.dispatchEvent("main-model", "save");
});
And in the form:
<xforms:model xxforms:offline="true" id="main-model" xxforms:external-events="save">

    <!-- Event received we dispatch when the form is taken online -->
    <xforms:action ev:event="save">
        <xforms:send submission="save-submission"/>
    </xforms:action>

    <xforms:submission id="save-submission" method="post" action="/save" replace="none">
        <xforms:action ev:event="xforms-submit-error">
            <!-- If saving fails, stay offline -->
            <xxforms:offline/>
        </xforms:action>
    </xforms:submission>
    
</xforms:model>
Notes:
  • You can run the <xxforms:offline/> action only:

    • As a result of an event dispatched when beforeOnlineListener is called.
    • Inside a <xforms:trigger> as shown earlier.

    You can look at running <xxforms:offline/> as a result of an event dispatched when beforeOnlineListener is called, not as really going offline, but rather as canceling a previous order to go online. This your way of telling the XForms engine: "something went wrong, abort going back online".

  • When you use <xxforms:offline/> this way to abort going online, you can run other actions which update the form as seen by users. For instance, as in the example below, you might want to give some feedback to users as to why, even if they asked for the form to be taken online, the form is still offline.

    When executing actions while canceling an order to go online, changes to instances done by those actions are not persisted. So for instance, in the example below, assuming the form is visible when taken offline (i.e. not taken offline from a summary page), and assuming message is shown in the view, the error will show in the form right after going online is canceled. But if users close the window, and reopen the form in a new window, that error won't be shown.

    Practically, you can use this feature to display error messages or run JavaScript to give feedback to users about what happened. But you should not use this, for instance, to auto-correct some of the values entered by users, as those auto-corrections will be lost if users close the offline form and later re-open it.
<xforms:submission id="some-submission" method="post" action="/some-service" replace="instance">
    <xforms:action ev:event="xforms-submit-error">
        <!-- Go back offline -->
        <xxforms:offline/>
        <!-- Give some feedback about what happened to users -->
        <xforms:setvalue ref="message">
            Something went wrong when going online.
            You are still offline.
        </xforms:setvalue>
    </xforms:action>
</xforms:submission>

Checking the Online/Offline Status

When your function formOnlineListener is called, if going online succeeded, at that point information about the form is removed from the Gears database. Calling ORBEON.xforms.Document.isFormOffline() returns the status of a form by checking its state in the Gears database. So you could expect that if your formOnlineListener calls isFormOffline() would return false if the form was successfully taken online. Unfortunately, that is not necessarily the case as updates to the database are not immediately committed, and call to isFormOffline() done right after the form was taken online from a different window can potentially return stale data. So if you wish to check if the form was successfully taken online from formOnlineListener, call isFormOffline() in the context of the form rather than in the context of the summary page:

ORBEON.xforms.Document.takeOnlineFromSummary(FORM_URL, function(formWindow) {
    formWindow.ORBEON.xforms.Document.dispatchEvent("main-model", "save");
}, function(formWindow) {
    var isFormStillOffline = formWindow.ORBEON.xforms.Document.isFormOffline(FORM_URL));
    // Your code using isFormStillOffline
});

Offline Data Encryption

By default data saved offline is not encrypted. To enable encryption, call ORBEON.xforms.Document.setOfflineEncryptionPassword(password). This function takes only one parameter: the “password” you want to use. If you want to use encryption, your application will need to ask the user for a password and call this API. No minimum length is imposed on the length of the password by the API, but your application should impose a reasonable minimum length.

You only need to call this API once per browser session, as the password will be stored the encryption password in a cookie, which is deleted when users exit the browser. The password is stored in the cookie is slightly obfuscated (stored in hexadecimal) to minimize the risk of someone seeing the values of cookies to be able to memorize the password and use it later.

To change the password (i.e. re-encrypt all the data stored in the database with a new password), call the API ORBEON.xforms.Document.changeOfflineEncryptionPassword(currentPassword, newPassword).

Both setOfflineEncryptionPassword() and changeOfflineEncryptionPassword() will check that the provided password matches the current password. (They do this by storing the password encrypted by itself in Gears.) If the current password is lost, there is no way to recover the events stored in the database with that password.

Getting Control Values

When creating the summary page, you might want to display some information specific to each form, typically the value of some key form controls. For this use the following API: ORBEON.xforms.Document.getOfflineControlValues(url), where url is the full URL of the form. This returns a map from control ID to value for that control. For instance, if you have a control ID my-control, you can get its value with:

var controlValues = ORBEON.xforms.Document.getOfflineControlValues(url);
var myControlValue = controlValues["my-control"];

For optimization reasons, this API returns the value for a subset of the form controls. For a control to be included in this subset, make sure you have a <xforms:bind> declared with a “name” attribute, and “nodeset” attribute pointing to the same node your control is bound.

This API returns the current value of the control, including changes to the values that happened:

  • After the form was loaded but before it was taken offline.
  • After the form was taken offline and after the “offline save” button has been pressed (i.e. changes that have been “committed” by end users).

Lower-level Considerations

Clearing the Content of the Gears Database and Store

If you wish to clear the content of your Gears database, run the ORBEON.xforms.Offline.reset() from the JavaScript console.

Running SQL Commands

You can check the content of the database with SQLite from the command line:

  1. Go to the directory where the Gears file are stored. The Gears FAQ will help you locate this directory. On a Mac, this directory will look like /Users/avernet/Library/Caches/Firefox/Profiles/uvjtqilb.default/Google Gears for Firefox/localhost/http_8080
  2. Run your SQL command, for instance: sqlite3 orbeon.xforms#database "select * from Current_Password"
Comments