Remembering States

Are you a back button snob? It’s ok if you are. We all should expect to return to a web page in the exact state we left it—searches preserved, tabs open, and the scroll state queued up. Think about entering a familiar room and finding it in a “reset” state…you would suspect you have a cleaning-obsessed poltergeist living with you. Creepy. It’s just not natural, and we shouldn’t let the internet act that way either.

The solution here is to make smart use of the browser’s history. And by smart, I mean use a really good plugin. Even though it’s just a simple matter of playing around with the history.pushState() and history.replaceState() methods, there is some wonky business between browsers that will save you time if you just avoid it. Our site was already using the jQuery BBQ plugin elsewhere, so I did the same.

I worked on storing the columns a user selects to be shown in a table. I wanted the same columns to reappear if the user left the page and used the back button to return to the table.

Each time a column was selected I pushed it to a column array used for the session, and also called an addToHistory() function, which turns the variables into a key/value pair that is passed to $.bbq.pushState(state[key] = val). Here is the coffeescript:

addToColumns: (data) => 
  if @existsInColumns(data)
    return
  else
    @columns.push(data)
    addToHistory "column", @columns

Similarly, each time a column was deselected, I removed it from the array and pushed a new key/value pair to the pushState() function.

Then, when the user leaves and comes back to the page, the browser handles the rest by accessing what we had pushed to its history state via the jQuery BBQ plugin. So easy.

Remembering States Forever

Remembering the page state one page back is all good, but sometimes a great user experience calls for remembering page state between sessions. Cookie time.

Persisting data between sessions is very similar to the idea of pushing the state to the browser history, except that here, the care, feeding, and retrieval of the data is all up to us…the browser doesn’t do anything for us except store the glorified string we give to it.

setCookie: ->
  date = new Date()
  date.setTime(date.getTime()+(3000*24*60*60*1000))
  expires = "; expires=#{date.toUTCString()}"
  data = []
  data.push(@columns) if @columns[0]

  document.cookie = "state=#{data}#{expires}"

Just as before, each time a column is selected, we want to make sure we take note of that change and add it to the cookie. Adding to the code from before, I just tacked on my setCookie() function at the end:

addToColumns: (data) =>
  if @existsInColumns(data)
    return
  else
    @columns.push(data)
    addToHistory "column", @columns
    @setCookie(@columns)

And, again, if a column is deselected, I just rewrite the cookie with the new column array.

With those in place, the next thing to consider is what happens when the page loads. Before any data is displayed, we want to check for a cookie, and if it exists, parse out the data into a usable form. Additionally, we also want to set the current page state, so it is in place in case the user wants to use the back button, utilizing the browser history as above. Something like this:

loadTargetState: ->
    # get the cookie data
    data = window.cookies
    return unless data
    
    # parse the cookie data
    columnState = data.filter (x) -> x.match(/^((?!:).)*$/)

    # store the state in this class, push history state
    @columns.push(column) for column in columnState
    addToHistory "column", columnState
    
    # pass the parsed data to the page
    @populateCheckboxes(column) for column in columnState

Creative Remembering

The best analogy I can think of for this next example is to have you imagine a room full of cryogenically-sealed page states. You can select one, thaw it, and see what the page looked like at the time that state was created.

We put this method to use to give users the ability to edit complex searches they performed on their data set. Users have the ability to save searches, and then they can later edit a search or save a new search based on an existing search. I added code to preserve the search state between pages and sessions, very similar to the columns state already demonstrated here.

Then, it was just a matter of piggy-backing off the existing cookie to recreate the search state selected from the saved searches page.

I made use of the same cookie, but could have easily created a second one, or used the URL to pass data. It’s just a matter of how you want to parse those strings.

From the saved search page, when the user clicks to edit a search, I added the search object ID to my cookie just before redirecting to the index page that displayed the data. I then used that search object ID to serve as a flag for being in “Edit Mode,” setting off a few conditionals if the search ID exists:

  • replace the page’s search filter with the search we are editing
  • replace some page text so it appears in “Edit Mode”
  • remove the search object ID from the cookie as soon as the user leaves the page, whether they save their changes or not. The search state will be preserved in the cookie, but the flag will be gone, so the page will not appear in “Edit Mode” when they return.

And that’s it. Three ways to remember page state. Your users will thank you!