Blog-Archiv

Donnerstag, 25. Dezember 2014

Preserve Inputs across Page Reload via JS

I needed to reload an HTML page, internally from JavaScript, but didn't want to lose the inputs the user might have made. A browser's page reload will remove any JS code and context completely. There is no global variable or hidden element that survives a page reload where I could store my settings into.

URL parameters seem to be the only way to overcome such a situation. The following shows some JS code that provides saving and restoring of input fields using URL parameters.

The Problem

The HTML code below exposes a "Layout" button that executes the JS function pageScript.toggleLayout() when the user clicks it. The two checkboxes with id="calculateMaxiumum" and id="logging" are to customize the behavior of the layout() function, and should be preserved across the page reload. (For the real-world example look at my demo page.)

    <div>
        <input type="button" value="Layout" onclick="pageScript.toggleLayout(this);"/>
        <br/>
        <input type="checkbox" id="calculateMaximum" checked="bydefault">Use maximum width</input>
        <br/>
        <input type="checkbox" id="logging">Log to console</input>
    </div>

And below is the code of the pageScript.toggleLayout() function. When first called, it changes the layout by calling the internal layout() function. It then changes to a "layouted" state, and next time it is clicked it reloads the page to restore the former layout. In this case it tries to keep the values of the checkboxes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script type="text/javascript">
   var pageScript = function() {
     var layouted = false;
     
     var toggleLayout = function(button) {
       if (layouted) {
         var calculateMaximum = $("#calculateMaximum").prop("checked"); // save checkbox values
         var logging = $("#logging").prop("checked");

         location.reload(); // reload page

         // following won't work to restore saved values!
         $("#calculateMaximum").prop("checked", calculateMaximum);
         $("#logging").prop("checked", logging);
       }
       else {
         layouted = true;
         layout();
         button.setAttribute('value', 'Reset Layout'); // set new label on button
       }
     };

     var layout = function() {
       ....
     };

     return {
       toggleLayout: toggleLayout
     };
   }();

</script>

Mind that I use jQuery in this code, all $(...) expressions are DOM queries via CSS expressions.
Mind further that this applies the "Revealing Module Pattern" to hide any implementation and expose just what is needed. The pageScript variable is the result of an anonymous self-executing function returning an object that exposes just toggleLayout(), all other variables and functions are hidden within the self-executing function.

This JS code calls the layout() function when not yet done, and reloads the page (to restore the previous layout) when already done.

It shows my first naive attempt to save the checkbox values. But this does not work, because after location.reload() the running JavaScript and its execution context (the window object) have been removed from the browser's memory!

So I needed some other approach to save and restore my checkboxes. The only way to save and restore page settings across a page reload are URL parameters. Because my page is a simple one that is not part of some web application (which already defines URL parameters) this is easy. I have control over the URL to be loaded, and can edit it in any way.

The URL of the currently loaded browser page is in JS variable location.href, which is a read/write variable, meaning that an assignment to location.href will load the assigned URL.

URL Parsing

This is needed when saving and restoring values via URL parameters. URL (Unified Resource Locator) and URI (Unified Resource Identifier) have been specified in so-called RFC ("Request For Comment"), see various sources on the internet for that. URL parsing is not "trivial".

Fortunately any web browser provides URL parsing in "a" elements (hyperlinks).

    var parseUrl = function(url) {
      if ( ! url  )
        return location;
        
      var hyperLink = document.createElement("a");
      hyperLink.href = url; // triggers URL parsing
      return hyperLink;
    };

This gives me access to the rough parts of an URL, which are the following for an example like

parseUrl("http://host:8080/path1/path2/servlet?one=1&two=2#somehash")
  • hyperLink.hash = #somehash
  • hyperLink.host = host:8080
  • hyperLink.hostname = host
  • hyperLink.href = http://host:8080/path1/path2/servlet?one=1&two=2#somehash
  • hyperLink.pathname = /path1/path2/servlet
  • hyperLink.port = 8080
  • hyperLink.protocol = http:
  • hyperLink.search = ?one=1&two=2
You can play with URL parsing on my demo page.

But as URL parameters are bundled with a starting "?" and "&" separators, like in ?one=1&two=2, I needed more functionality to access the values of such parameters.

Furthermore care has to be taken to encode and decode their values, because not any character is allowed in an URL (see RFC specification).

URL Parameter Access

The URL parameters are packed in the hyperLink.search variable. Here are some JS functions to manage URL parameters in relation with the parseUrl() function above.
They do not depend on jQuery (nice to copy & paste :-).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    /**
     * Reads the value for given parameter name from given params string, containing all parameters.
     * @param name the name of the parameter.
     * @param params the string containing all URL parameters, composed like "location.search".
     * @return the value of given parameter.
     */
    var getUrlParameter = function(name, params) {
      params = params || location.search; // default to current URL parameters
      
      var match = new RegExp("[?&]"+name+"=([^&]*)").exec(params);
      if ( ! match )
        return undefined;
        
      var value = match[1].replace(/\+/g, " "); // replace every '+' by a space
      return decodeURIComponent(value);
    };
    
    /**
     * Sets the given parameter name and value to given URL string,
     * overwriting any existing value. Removes the parameter when value is empty.
     * @param name the name of the parameter to set.
     * @param value the value of the parameter to set.
     * @param url the URL, as text, where to set the parameter into.
     * @return a new URL containg the given parameter.
     */
    var setUrlParameter = function(name, value, url) {
      var parsedUrl = parseUrl(url);
      var params = parsedUrl.search;
      if (params) {
        var match = new RegExp("[?&]"+name+"=([^&]*)").exec(params);
        if (match) { // remove old name=value
          var start = params.indexOf(match[0]);
          var end = start + match[0].length;
          params = params.slice(0, start) + params.slice(end);
        }
        if (value)
          params = params+"&"+name+"="+encodeURIComponent(value);
      }
      else if (value) {
        params = "?"+name+"="+encodeURIComponent(value);
      }
      var href = bareUrl(parsedUrl);
      return href + params + (parsedUrl.hash ? parsedUrl.hash : "");
    };
    
    /**
     * Removes all URL parameters from given URL.
     * @param url the URL, as text, where to remove all parameters from.
     * @return a new URL containg no parameter.
     */
    var removeAllUrlParameters = function(url) {
      var parsedUrl = parseUrl(url);
      var bare = bareUrl(parsedUrl);
      return bare+(parsedUrl.hash ? parsedUrl.hash : "");
    };
    
    /** @return given parsed URL object as text without parameters and hash. */
    var bareUrl = function(parsedUrl) {
      var href = parsedUrl.href; // contains both params and hash
      href = parsedUrl.search ? href.slice(0, href.length - parsedUrl.search.length) : href;
      href = parsedUrl.hash ? href.slice(0, href.length - parsedUrl.hash.length) : href;
      return href;
    };

This should be everything needed to reload a page and preserve input values. The functions, their parameters and return values are decribed in heading comments.

Notes:

The replacing of every '+' by a space is to overcome some older browser's URL space treatment.

You find documentation about regular expressions on the internet.
In short, the RegExp("[?&]"+name+"=([^&]*)") reads

Starting with '?' or '&', then the parameter name, then '=', then zero-to-n characters that must not be '&'.
The parentheses around ([^&]*) are to tell the RegExp that the value of the parameter behind the '=' should be returned in the result array.

The built-in JS functions encodeURIComponent() and decodeURIComponent() have been used to pack and unpack any possible value that is stored into an URL parameter.

Integration

Following shows how my in-page script looked like after integrating these functions.
Here I use jQuery again to access the checkboxes to preserve.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<script type="text/javascript">
   var pageScript = function() {
     var layouted = false;
     
     var toggleLayout = function(button) {
       if (layouted) {
         reloadPage();
       }
       else {
         layouted = true;
         layout();
         button.setAttribute('value', 'Reset Layout'); // set new label on button
       }
     };

     var layout = function() {
       ....
     };
     
     // above URL managing functions go here
     // ....

     var reloadPage = function() {
       var urlText = removeAllUrlParameters();
       
       var calculateMaximum = $("#calculateMaximum").prop("checked");
       urlText = setUrlParameter("calculateMaximum", calculateMaximum ? "true" : "false", urlText);
       
       var logging = $("#logging").prop("checked");
       urlText = setUrlParameter("logging", logging ? "true" : "false", urlText);
       
       location.href = urlText; // reload page
     };
    
     $(document).ready(function() {
       if ( ! location.search )
         return; // no URL parameters given
       
       var calculateMaximum = getUrlParameter("calculateMaximum");
       if (calculateMaximum)
         $("#calculateMaximum").prop("checked", calculateMaximum === "true");
       
       var logging = getUrlParameter("logging");
       if (logging)
         $("#logging").prop("checked", logging === "true");
     });
     

     return {
       toggleLayout: toggleLayout
     };
   }();
   
</script>

You find the URL-managing functions and an example application on my demo page.

For a more demanding JS library to handle URLs and URL-parameters you might want to have a look on URI.js.




Keine Kommentare: