Invisbly remember state with Ajax
July 22nd, 2007
A few times now I have used a useful pattern. The problem is that I want to remember the state of a javascript driven effect across multiple pages. For instance, one of my projects has a context sensitive administration pane that can be shown or hidden on any page. I want someone to be able to show this admin pane, go to another, and have the admin pane for that page already opened. Then I want them to be able to close the pane, and have it stay closed on all pages until it’s manually opened again.
How do manage this, when the opening and closing of the pane is entirely client side driven javascript? With Ajax, of course!
Let’s take this as a basic example:
1 2 3 4 5 |
<%= link_to_function 'Show Admin', "$('admin').show()" %> <div id="admin"> <h2>Admin!</h2> <%= link_to_function 'Hide Admin', "$('admin').hide()" %> </div> |
The problem is the server has no way of knowing whether the admin was shown or hidden when a page is loaded. All we need is a little Ajax sprinkle to send a note to the server when the state changes. So lets try it this way instead:
1 2 3 4 5 6 7 8 9 |
<%= link_to_remote 'Show Admin', :url => { :controller => 'admin', :action => 'remember', :id => true }", :loading => "$('admin').show()" %> <div id="admin"> <h2>Admin!</h2> <%= link_to_remote 'Show Admin', :url => { :controller => 'admin', :action => 'remember', :id => false }", :loading => "$('admin').hide()" %> </div> |
Now all you need is for the remember action to save the state in a session variable like session[:admin_state]. You can then look at this when rendering your views and determine how to display things form there.
The real beauty here is the Ajax request is completely invisible to the user. The javascript that manipulates the page is executed right away, but the Ajax request is fired off asynchronously in the background, and the result of it doesn’t matter. In fact the action the request is calling should have a render :nothing => true for efficiency. There is no delay waiting for the server, so UI responsiveness is immediate, but magically the server remembers the state.
And for the more adventurous, my real solution is here, in HAML and Javascript
View:1 2 3 4 5 6 7 |
#admin
=link_to_function('Show Admin', 'App.admin.show()', { :id => 'show_admin_link' }.merge(session[:show_admin] ? { :style => 'display: none' } : {}))
#admin_content{ session[:show_admin] ? {} : { :style => 'display: none' } }
#admin_content_padding
%h2 Administration
... admin stuff here ...
=link_to_function 'Hide Admin', 'App.admin.hide()', :id => 'hide_admin_link' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// public/javascripts/application.js App = { admin: { show: function() { $('show_admin_link').hide(); $('admin_content').visualEffect('blindDown'); this.remember(true); }, hide: function() { setTimeout(function() { $('show_admin_link').visualEffect('appear', { duration:0.3 }); }, 1000) $('admin_content').visualEffect('blindUp'); this.remember(false); }, remember: function(status) { new Ajax.Request('/admin/status/' + status, { asynchronous:true }) } } }; |
Note how this javascript will automatically make the Ajax request when when told to show or hide. So all I have to do in my view to handle the effects, and the Ajax, is App.admin.show() or App.admin.hide().
Beautiful.
Leave a Reply