stafffan
stafffan

Reputation: 492

How can I store functions within the HTML5 history states

So I'm using the HTML5 history management for adding the ability to navigate back and forward within a website with AJAX loaded subcontent.

Now I would like to store javascript functions within the state object, to callback at the state popping. More or less like the following code:

$(window).bind("popstate", function(event) {
    var state = event.originalEvent.state;
    if (!state) {
        return;
    }
    state.callback(state.argument);
}

function beforeLoad() {
    var resourceId = "xyz";
    var func;

    if (case1) {
        func = switchPageToMode1;
    } else { // case 2
        func = swithPageToMode2;
    }

    func(resourceId); // run the function
    window.history.pushState({ callback: func, resourceId: resourceId }, "newTitle", "newURL"); // and push it to history
}

function switchPageToMode1(resourceId) {
    alterPageLayoutSomeWay();
    loadResource(resourceId);
}

function swithPageToMode2(resourceId) {
    alterPageLayoutSomeOtherWay();
    loadResource(resourceId);
}

function loadResource(resourceId) {
    ...
}

All right. So what I'm trying to do is storing a reference to a javascript function. But when pushing the state (the actual window.history.pushState call) the browser files a complaint, namely Error: "DATA_CLONE_ERR: DOM Exception 25"

Anybody knows what I'm doing wrong? Is it at all possible to store function calls within the state?

Upvotes: 4

Views: 4601

Answers (3)

Gili
Gili

Reputation: 90023

Here is what I do:

  • Each HTML page contains one or more components that can create new History entries.
  • Each component implements three methods:
    1. getId() which returns its unique DOM id.
    2. getState() that returns the component's state:

      {
        id: getId(),
        state: componentSpecificState
      }
      
    3. setState(state) that updates the component's state using the aforementioned value.
  • On page load, I initialize a mapping from component id to the component like so:

    this.idToComponent[this.loginForm.getId()] = this.loginForm;
    this.idToComponent[this.signupForm.getId()] = this.signupForm;
    
  • Components save their state before creating new History entries: history.replaceState(this.getState(), title, href);

  • When the popstate event is fired I invoke:

    var component = this.idToComponent[history.state.id];
    component.setState(history.state);
    

To summarize: instead of serializing a function() we serialize the component id and fire its setState() function. This approach survives page loads.

Upvotes: 0

futbolpal
futbolpal

Reputation: 1388

I came up with a slightly different solution.

I added two variables to the window variable

window.history.uniqueStateId = 0;
window.history.data = {}.

Each time I perform a pushstate, all I do is push a unique id for the first parameter

var data = { /* non-serializable data */ };
window.history.pushState({stateId : uniqueStateId}, '', url);
window.history.data[uniqueStateId] = data;

On the popstate event, I then just grab the id from the state object and look it up from the data object.

Upvotes: 2

Jordan Running
Jordan Running

Reputation: 106027

No, it's not possible, not directly anyway. According to MDC the "state object," i.e. the first argument to pushState, "can be anything that can be serialized." Unfortunately, you can't serialize a function. The WHATWG spec says basically the same thing but in many more words, the gist of which is that functions are explicitly disallowed in the state object.

The solution would be to store either a string you can eval or the name of the function in the state object, e.g.:

$(window).bind("popstate", function(event) {
  var state = event.originalEvent.state;

  if ( !state ) { return; }

  window[ state.callback ]( state.argument );  // <-- look here
}

function beforeLoad() {
  var resourceId = "xyz",
      func
  ;

  if ( case1 ) {
    func = "switchPageToMode1";  // <-- string, not function
  } else {
    // case 2
    func = "swithPageToMode2";
  }

  window[ func ]( resourceId );  // <-- same here

  window.history.pushState(
    { callback : func,
      argument : resourceId
    },
    "newTitle", "newURL"
  );
}

Of course that's assuming switchPageToMode1 and -2 are in the global context (i.e. window), which isn't the best practice. If not they'll have to be accessible somehow from the global context, e.g. [window.]MyAppGlobal.switchPageToMode1, in which case you would call MyAppGlobal[ func ]( argument ).

Upvotes: 7

Related Questions