webdev rails
(7,268 views)

Eat Gooey Cookies

How I'm using cookies to communicate non-critical user activity back to the server. My application and this example uses Ruby on Rails with jQuery for client-side javascript, and the jQuery cookie plugin. and JSON .

Suppose you have a page where the user can click a Help button that reveals extra instructions. You might implement this using a hidden DIV block containing the helpful text, and the help button or icon which when clicked, using javascript, will show/hide the block. OK, simple and common enough.

Then the user clicks a link, or refreshes the page, or goes away and comes back, or anything else that causes the page to reload, and now the help text is gone, its hidden again. The browser simply forgot (and the server was never notified) what the user had done before.

Maybe this is what you want. Or maybe it's not.

In more complex browser/javascript based GUI, the problem is even more pronounced, like client side table sort, resized boxes, font preferences, etc.

One solution is to Ajax a message to the server any time the user modifies elements on the page. But for non-critical GUI niceties, that seems excessive in a number of ways especially if the server doesn't need to respond to those events. Its all client-side activity.

My solution is to transparently stuff the preferences into a cookie, which the server can take note of upon the next request, and use, if necessary, in subsequent responses.

Baking the Cookies

Basically it works like this:

  • like a good boy, your application generates normal html, and you have javascripts unobtrusively sweeten the page after its been loaded.
  • The unobtrusive javascript adds client-side actions that may not notify the server.
  • Upon each user event, the browser (client side js) remembers changes to the gui state in a cookie.
  • When the user submits a new request to your application, the cookie is automatically passed along.
  • The app, before processing the request, reads the cookie, records the current user preferences in the database, and deletes the cookie. 
  • The app generates the next page, possibly using the current gui preferences as needed.

Bite My Cookie

Cookies are stored as files in the client browser's disk cache. Typically, the cookie function assumes one value per cookie. At first glance, it might be tempting to just use one cookie per user preference (e.g. one name:value pair per user preference). But while cookies are limited to 4k bytes, its also not a good idea to sprinkle many cookies for your site.

I maintain user preferences in a javascript object, user_gui, updated on any event that I want to record, and written to a "gui" cookie. The object is stringified using JSON, its a little verbose but convenient.

fyi, My application and this example uses Ruby on Rails with jQuery for client-side javascript, and the jQuery cookie plugin. and JSON .

Example

Lets start with an unobtrusive javascript example, but the current gui state is not recorded.

In the View template:

      <a href="/some/help" id="icon_23"><img src="images/myicon.gif"></a>
      <div id="div_23" style="display:none" >your stuff</div>
      
      <script>
        $(function(){ 
          $("#icon_23").click( function(){ 
$("#div_23).toggle();
return false;
})
        })
      </script>

First, we think of the poor guy who has no javascript and make sure the page still works for him. The icon is a hyper link to a separate help page. The DIV is hidden.

Then we bind a function to the onclick event on the icon, which toggles the visibility of the div. OK, thats jQuery101.

Example with Gooey Cookies

Now we take that example and record the current gui state so when the page is reloaded, the div will be visible or not as the last time the user visited.

      <a href="/some/help" id="icon_23"><img src="images/myicon.gif"></a>
      <div id="div_23" style="display:none" >your stuff</div>

      <script>
        $(function(){ 
          <% if current_user.gui["div_23"] %>
            $("#div_23").show();
          <% end %>
          $("#icon_23").click( function(){ 
$("#div_23").toggle('normal', guicookie );
return false;
})
        })
      </script>

Not very different. The HTML doesn't change at all. As for the javascript, first we initialize the current view state of the div based on current_user settings. (Note the use of server side ERB which conditionally includes the .show() statement only if its required. Or the conditional show() could be done in any number of other ways.)

Then, during the toggle, we add a callback to guicookie function, which records the state in the cookie. (The toggle function require we specify a speed argument, eg "normal", when also providing the guicookie callback function).

Ingredients

I've taken a convention-over-configuration approach to simplify things. Typically I just want to remember if something's visible or not, or tag it with some value.

We'll assume this "something" is a single DOM element, with a unique ID. That id will be used as the property name in the javascript object, and ultimately stored in the user record in the database.

The following methods are used to store the state in the cookie

var user_gui = {}
function guicookie( value ) {
  var key = $(this).attr('id');
  value = value || $(this).is(':visible');
  user_gui[key] = value;
  guicookie_save();
}
function guicookie_save() {
  var value = JSON.stringify(user_gui);
  if (value == '{}') value = null; // delete it if empty
  $.cookie('gui', value, {path : '/'} );
}

The guicookie function can be used in 2 ways. If no value is supplied, we check whether the element is visible, and store true or false in the cookie. When a value is provided, we assign that value to the cookie. The guicookie_save helper stringifies the object and updates the cookie.

On the server side, we need to consume the cookie. In Rails you can define a before_filter in the application controller (e.g. application.rb file) so the cookie gets consumed on any action.

     before_filter :eat_gui_cookie

     def eat_gui_cookie
        if logged_in? && cookies[:gui]
         current_user.gui.merge!( cookies[:gui].from_json ) 
         cookies.delete :gui
       end
    end

 This uses a little addition to the String class to decode the cookie string value,

  String
    def from_json
      ActiveSupport::JSON.decode(self)
    end
  end

Classy Cookies

There's more?! On the client (javascript) side I've added a guicookie_class function which lets you remember when an element's been tagged with a class name (eg using addClass). It create a new property named CLASS_DOM_ID which which will be true if CLASS is present.

For example, lets say an event has left your DOM with an element like <div id="item_123" class="flagged"> then guicookie_class will set user_gui["flagged_item_123"] = true and pass that back to the server in the cookie.

jQuery.fn.guicookie_class = function(clas){
  var key = clas+'_'+$(this).attr('id');
  user_gui[key] = $(this).hasClass(clas);
  guicookie_save();
}

 In this case its called as a jQuery function, eg

$("#item_123").toggleClass('flagged').guicookie_class('flagged'); 

 

Enjoy!

 

 

 

 

Comments

by Maurizio on Dec 08, 2008

>>

Nice article.

New Comment

markdown formatting permitted