Projects‎ > ‎XForms‎ > ‎

Browser back, reload, and cloning


Rationale

When users use the "back" functionality in their browser to come back to a form generated by Orbeon Forms, Orbeon Forms should, when possible, restore the form to its state just before users left the form. By extension, this also applies to the history "forward", "reload", and tab/window duplication.

Current implementation

As of 2010-05-25.
  1. Overview – When the page loads, Orbeon Forms attempts to detect whether this is a form that is freshly loaded from the server, or if this is a forms users are getting after using the browser "back" functionality. This detection relies on the browser restoring the values of form fields.

  2. Storing the initial dynamic state – When a page is loaded, Orbeon Forms stores the UUID provided by the server in the $uuid hidden text input field to the $client-state hidden hidden with CSS [because Chrome does not restore hidden fields] text field, along with information signifying that the form has been already loaded (load-did-run=true). When an Ajax request is sent to the server, the current sequence number is also incremented and stored in the client state. Those name/value pairs are stored in the $client-state in the following format:

    uuid&D8A74AA4-104F-C06A-60D1-CFA7B3DFE3A2&load-did-run&true&sequence&2

  3. Asking the server to resend events – When a page is loaded, Orbeon Forms checks if the $client-state contains a load-did-run=true, and if it does, it sends an Ajax request to the server asking the server resend all the events that happened since the page loaded, so the client can replay them. The Ajax request looks like:

    <xxforms:event-request xmlns:xxforms="http://orbeon.org/oxf/xml/xforms">
        <xxforms:uuid>D8A74AA4-104F-C06A-60D1-CFA7B3DFE3A2</xxforms:uuid>
        <xxforms:action>
            <xxforms:event name="xxforms-all-events-required"/>
        </xxforms:action>
    </xxforms:event-request>

Future improvement 

  • Using the YUI Browser History Manager – With the YUI Browser History Manager, Orbeon Forms can "store" information in the URL signifying that the form has already been loaded, e.g. by adding #uuid to the URL, where uuid is the document UUID for that page. Then when the user goes back to a page with #uuid, the client asks the server to replay the events that correspond to the current document UUID. This technique enables page cloning, which is a downside, but not a show stopper as page cloning will need to be handled as it is a feature of a number of modern browsers.

Browser back support

Current status

(As of 2010-05-25, verified on 2010-12-01)

Types of restore

(2010-05-25 brainstorm, updated 2010-12-01)
  1. Browser has the HTML in cache (but not the DOM)
    • This is the scenario that was implemented in OF initially (see issues above)
    • The server must send diffs between
      • Initial dynamic state (at time the page is sent to client)
      • AND the current dynamic state

  2. Browser reloads the page from the server (because it did not keep it in its local cache)
    • In general, this can break because the page can be completely different
    • A new document UUID is generated
    • If the static state of the page is the same, then a diff is possible between
      • Initial dynamic state of the new UUID
      • Current dynamic state of the old UUID
    • If the static state of the page is NOT the same, then the state should not be restored
    • NOTE: the UUID of the static state should probably be a hash instead, to facilitate caching

  3. Browser has DOM and related objects still in memory
    • This can happen with Opera, but we've never seen this with Chrome, Firefox, or IE
    • In the rare cases where this happens, there is nothing to be done: the state is already the "restored" state

Funny Chrome/Firefox restore of $uuid hidden field

[Added 2011-08-04]

See also:
What seems to be happening here is that on browser back the $uuid hidden field can be restored to a value that comes from another tab. The hidden $uuid field looks like this:

<input type="hidden" name="$uuid" value="73B134E2-AFBD-D2B4-1682-20E27BFA7D67">

The two browser tabs must have the same URL (e.g. http://localhost:8080/orbeon/fr/a/b/new), and they initially get different values for $uuid upon page load, as should be the case.

Upon browser back, it seems that Chrome/Firefox restore the value of the $uuid field based on the latest value this field had. So if just before the browser back, you load the same page in a different tab, that new value will be used to restore the field in the first tab.

Specific steps:
  • create a form with single field with FB, publish, and enable the workflow-send button to send to the echo service
  • load the form in tab 1
    • this creates a new XForms document, say 1C897014-9EF7-184B-03A0-78C4D1C7A04B
    • enter a value in the field
    • press workflow-send
    • the send submission runs as expected, with the two passes
  • in tab 2 load the same form
    • this creates a new XForms document, say 262A0E45-0D30-10C9-239B-9EE6AA09448D
  • do a browser back in tab 1
    • Chrome/Firefox restore our UUID hidden form field with 262A0E45-0D30-10C9-239B-9EE6AA09448D instead of 1C897014-9EF7-184B-03A0-78C4D1C7A04B
    • it's as if the hidden form fields were stored per URL, and the last fields only were kept
    • press workflow-send
    • the send submission runs
    • the first pass works as expected
    • the second pass does an HTML form post, and sends the wrong uuid: 262A0E45-0D30-10C9-239B-9EE6AA09448D
    • this is then as if the page in the second tab was sending its data, which is blank!
The UUID obtained from the $client-state field upon browser back is correct, so Ajax requests work. The $uuid field is used only upon HTML form submissions, which means 2nd pass of two-pass submission, and (it seems) file uploads.

A fix for this was implemented on 2011-08-04. The idea is, upon browser back to restore the value of the $uuid field based on the value found in $client-state.

There are 2 open questions:
  1. Can we simply make $uuid a text input field hidden with CSS, like $client-state?
  2. Can this fail in Noscript mode as well? In which case the JavaScript fix above wouldn't work. If so, would making $uuid a text input field hidden with CSS fix this as well?
NOTE: This seems to contradict the statement that Chrome doesn't restore hidden field! Maybe Chrome only stores the value they got upon page load, but values modified with JavaScript?

Tab/window duplication

As of 2010-05-25, it doesn't seem like current browsers perform exact copies browser tab/windows:
  • The current state of the DOM and JavaScript objects is not kept.
  • The browser history is kept.

Browser Supports tab/window duplication
How to clone a page
Restores DOM/JavaScript objects
Shows DOM as when first loaded
Sets form fields to value at time of cloning
Chrome
Yes
Right click on a tab and select Duplicate. No
Yes
Yes
Firefox
Yes 
Drag a tab to a new location pressing the option key on OS X or the control key on Windows.
No
Yes
Yes
Opera
Yes
Right click on a tab and select Clone tab
No
Yes
No
IE 6/7
No 
-
-
-
-
IE 8
Yes
"Duplicate tab" feature (ctrl-K).
No
Yes
No

Browser reload button

Problems

  • browser typically issues an HTTP request
  • browser might restore form fields (Firefox)
  • browser might not restore hidden form fields (all browsers other than Firefox)
    • => relying on form fields to detect change is not reliable
  • static state might have changed

Solutions

  • use instead YUI history manager
    • benefits:
      • better detection
    • drawbacks
      • URL is modified
      • NOTE: browser supporting HTML5 will later be able to store state outside the URL
    • use of history manager
      • either upon page load, or as soon as user creates
  • server gets new HTTP request
    • produces new HTML
    • produces new doc UUID
    • (this works now)
  • upon JS init
    • if UUID in URL
      • there was an interaction w/ the page
      • client sends Ajax request
    • if no UUID in URL
      • there was no interaction w/ the page, so don't send Ajax request
    • Ajax request
      • UUID is new doc UUID stored in page, as for any Ajax request
      • must send URL doc UUID as param to xxforms-all-events-required
    • server
      • checks static state UUID for both doc UUIDs
      • if different static states
        • XForms code changed on server
        • OR static state was not found in cache
        • server sends initial value of all fields for new doc UUID
      • if same static state
        • send diff between
          • new doc UUID initial state
          • old doc UUID current state
      • server generates new doc UUID
    • client
      • processes Ajax response
      • does location.replace with new doc UUID
NOTE: This solution works for all cases: history navigation, reload, and duplicate.

Constants

  • we never add to history, but use location.replace
  • UUID must be stored in HTML/JavaScript, because form fields are not reliable
  • UUID in #hash represents the UUID of the current state
    • use as UUID in Ajax requests

Implementation steps for improved reload/restore/etc.

  • on load, client must detect hash
  • <xxf:xxforms-all-events-required>
    • rename to <xxf:xxforms-diff>
    • new attribute: <xxf:xxforms-diff target="target-uuid"/>
    • server must support @target
  • server must be able to clone state
  • server must be able to send new UUID in response
    • <xxf:uuid>...</xxf:uuid>

Pending issues

Full state of the page is not restored as xxforms:script don't run on browser back

See bug: [ #315751 ] JavaScript for XBL components does not run on browser back/reload. On 2011-01-19, we brainstormed about this issue, and came up with the following solution:
  1. The JavaScript object corresponding to a component can implement a method that corresponds to a "back event" (let's assume it is called xxformsBack()).
  2. When the user goes back, after the DOM is restored (as it is now), the object init() and xxformsBack() are called.
  3. For this to be possible, there needs to be an explicit mapping from an XBL component to the JavaScript class, for instance by adding an attribute on <xbl:binding>:

    <xbl:binding id="fr-button" element="fr|button" xbl-class="YAHOO.xbl.fr.Button">

  4. Since the server knows what the class is, on back, for each component it can send:

    <xxf:script name="back_button_xforms_function" target-id="my-button" observer-id="my-button"/>

    Where back_button_xforms_function is a function it produced in the page, which does YAHOO.xbl.fr.Button.instance(this).xxformsBack();.

  5. Optionally, in the cases where the class corresponding to a component is defined, the component author, instead of writing:

    <xxforms:script ev:event="xforms-readonly" ev:target="#observer">
        YAHOO.xbl.fr.Button.instance(this).readonly();
    </xxforms:script>

    Could just write:

    <xxforms:xbl-script ev:event="xforms-readonly"/>

    The server would tell the client:

    <xxf:xbl-script xbl-class="YAHOO.xbl.fr.Button" event="xforms-readonly" target-id="my-button"/>

    And the xformsReadonly() method would get called. This way, we wouldn't have to generate methods in the markup of every page, for each event handler, for each XBL component type used in the page. As a nice side-effect, this would also make debugging easier by removing one level of indirection.

Browser back not restoring the form state in IE with Cache-Control: no-cache

Setting the Cache-Control to no-cache solves a rare problem on IE where the browser over-aggressively caches HTML produced by Orbeon Forms even though Expires is set to be equal to Date. However, this breaks restoring the form state on back as IE does not restore the form fields when Cache-Control is set to no-cache.

A possible solution to be tested is to instead set Cache-Control to private, max-age=0 (which is also what Google does on their home page). We still need to confirm that this solves the over-aggressive caching behavior.

Browser back not restoring the form state in Chrome

At this time, support for the browser "back" does not always work on Chrome. In some cases Chrome caches the DOM of the page. In those cases, when hitting back, no request is send to the server, and the page is shown in the state in which it was when it was last shown. In other cases, Chrome will fetch the page from the server and restore the value of the form fields like <input type="text">, but not the value of hidden fields. Hence no Ajax request is sent to the server to replay the events, and the page is left in a potentially inconsistent state.

Back broken in noscript mode when going back more than 1 step

Solution:
  • noscript mode must serialize AND keep all dynamic states?
  • or store dynamic state in HTML response? (client state)?

When cloning, two windows ending up with the same UUID

Alternative consequence of duplicating:
  1. Multiple views of the form data
    • no "forking" of doc UUID
    • benefits:
      • cool collaboration feature
    • drawbacks:
      • more work needed, need to support multiple users accessing this doc, not a requested feature so far
    • => could be an option in the future
  2. Restore page as shown by browser, i.e. initial state w/ possibly restored form fields
    • drawbacks:
      • not a good idea, see case of wizard: fields might have been filled out, but DOM/wizard state won't be restored
  3. Restore page as in state of source tab
    • algo
      • client knows that duplicating occurred (using browser history #uuid URL fragment)
      • client asks server for diff
      • server generates new doc UUID for new tab
      • send diff between
        • new doc UUID initial state
        • old doc UUID current state
      • even if client updated form fields, server will send the latest so things fine
      • NOTE: request counter was considered; prob. not useful for this, but could be useful to handle Ajax retries
    • benefits:
      • easy to understand from user perspective
    • drawbacks:
      • can't make distinction between duplicate, reload, and history navigation (browser back); might be possible w/ HTML5
    • => best solution so far

Resolved issues

New UUID sent on Firefox 4

Resolved as of 2010-12-01.

With Firefox 3.6, when going back, the HTML is not retrieved from the server, so the UUID stays the same. But it seem that Firefox 4 always retrieves the HTML, and so gets a new UUID. The UUID is stored in an <input name="$uuid" type="hidden" value="9050…">, but its value is not restored, maybe because Firefox only restores the value of fields that changed. Hence, the next time we send an Ajax request, we send it with the new UUID instead of the old one.

The solution is to store the UUID in the client state when the page is loaded, if there is not one stored there already. Then when sending a request, we always do so using the UUID from the client state. Since the client state field changes, it is properly restored by Firefox 4 even when it reloads the HTML from the server.


Issue of two states in client history

[NOTE: no longer an issue if using location.replace]
  • Issue:
  • with solution above, first URL is e.g. /foobar, then after first Ajax interaction, /foobar#hash
  • this means user can go back to /foobar or forward to /foobar#hash: what happens then?
  • Possible solution
  • in the same way that the server has "initial" and "current" state, client can reflect this
  • going back from /foobar#hash to /foobar
    • client detects this
    • client tells server to play events back
    • server diffs current -> initial (reverse of usual diff)
    • server serializes current state to store
    • upon next user interaction
      • client: #hash is added again as usual
      • server: business as usual
  • going forward from /foobar to /foobar#hash
    • client detects this
    • client tells server to play events back
    • server diffs initial -> current
    • upon next user interaction
      • client: #hash stays as is
      • server: business as usual

NOTE: this is probably not ideal, but better than not doing it.

Comments